У меня уже был шахматный интерфейс для сайта. Можно было выбрать игру с другом или компьютером, настроить время и открыть доску. Потом в Telegram появились Widgets, и я решил перенести туда игру целиком — чтобы выбор режима и сама партия помещались прямо в сообщении.
Больше всего времени в итоге ушло на доску, фигуры и попытки сохранить нормальный интерфейс в очень небольшом объёме кода. Несколько раз приходилось возвращаться к уже работающему экрану и переделывать его ещё раз.
Сейчас лобби занимает 3 395 байт после сжатия, а доска — 3 957. До предела у доски осталось 139 байт. Расскажу, как к этому пришёл и что пришлось урезать.

Что Telegram добавил в API
Метод sendWidget появился в Bot API 10.4 от 9 ноября 2026 года. Бот передаёт HTML, который Telegram показывает внутри сообщения. Для нажатий есть widget_query, для ответа — answerWidgetQuery, для изменения состояния — editMessageWidgetData. Bot API 10.4
Для моей игры это означало, что можно отправить доску один раз и дальше менять её состояние. Сам HTML в уже отправленном сообщении остаётся прежним. Обновлённую разметку приходится отправлять новым виджетом.
Сначала я хотел сохранить весь интерфейс одним документом. Лобби, настройки, доску и действия во время партии. В браузере такой экран выглядел нормально. При сборке оказалось, что одним HTML здесь дело не ограничивается — его ещё нужно уместить в сжатый бюджет.
Из ограничений мне чаще всего приходилось учитывать следующие.
Что ограничено |
Лимит |
|---|---|
HTML до сжатия |
32 768 байт UTF-8 |
HTML после zlib уровня 9 |
4 096 байт |
|
По 1 024 байта |
Запрос от виджета |
64 байта |
Высота сообщения с виджетом |
До 600 px |
Узлы DOM |
До 2 000 |
Тяжёлые эффекты одновременно |
До 16 элементов |
У виджета также закрыт доступ к сети, а внешний код, картинки и шрифты не загружаются. Поддерживаемые HTML и CSS перечислены в профиле виджета. Документация и лимиты
Отсюда появились вполне конкретные решения. Крупную обложку я вынес в отдельное приветственное сообщение. Фигуры на доске сделал векторными. Основной экран оставил под игру, а настройки начал уплотнять и разделять.
Весь интерфейс в один файл не поместился
В одном из вариантов сборка показала 4 940 байт. Это был замер скрипта на Node.js. Экран уже умел то, что мне было нужно, но Telegram такой размер не принимал бы.
Дальше можно было продолжать вырезать по кнопке. Но в одном месте нужны сложность и цвет, в другом — часы, превращение пешки, подтверждение сдачи и реванш. Эти элементы относятся к разным этапам игры.
Я разделил интерфейс на два документа. В лобби остались режимы и настройки. В доске — партия и действия с ней. Оба экрана собираются из общего исходника widget-source.html. Скрипт передаёт Terser признак экрана, и код другой части удаляется из результата.
На выходе получаются lobby.html и board.html. При запуске партии бот отправляет доску и удаляет предыдущий виджет. После этого ходы обновляют состояние того же сообщения.
Так удалось сохранить выбор режима внутри виджета. На раннем этапе настройки уже оказывались обычными кнопками в чате, и результат сильно отличался от того, что было на сайте. После разделения экранов они вернулись в общий интерфейс.
За это пришлось заплатить переходом через бот между лобби и доской. Зато выбор цвета, сложности и времени происходит локально — нажимаешь кнопку и сразу видишь выбранное значение.

Выбор сложности и цвета перед началом партии с компьютером.
Что я убрал из оформления
На главном экране сначала был белый конь, небольшая шахматная площадка и анимация появления. В браузере композиция выглядела аккуратно. В клиенте Telegram конь получился смещённым относительно площадки.
Я убрал эту иллюстрацию. Сейчас в шапке лобби только название Master Castle, ниже три кнопки режимов. Зелёная кнопка выделяет игру с другом. В этом месте важнее быстро выбрать, с кем играть.
Большая обложка с фигурами осталась в приветствии бота. В лимит HTML игрового виджета она не входит.

Приветственное сообщение отдельно от игрового виджета. Снимок сделан с аккаунта администратора.
История ходов тоже осталась за пределами основного экрана. Партия хранится в SQLite и доступна для экспорта в PGN. Под доской находятся строка состояния и кнопка действий. Это освободило место под сами клетки и помогло уложиться по высоте.
Фигуры пришлось переделывать отдельно. В промежуточных версиях они получались размытыми, а при некоторых вариантах отрисовки съезжали к краям клеток. В итоге я использовал упрощённые SVG-фигуры на основе Cburnett. Векторные определения создаются один раз, а на клетках стоят ссылки use.
Набор путей тоже занимает место. Его первые 1 024 байта передаются в public_data, оставшаяся часть включена в HTML. На стороне виджета эти части собираются обратно. Поэтому в окончательном замере нужно учитывать уже подставленные векторы.

На снимке партия с компьютером, которая идёт без часов. В игре между людьми рядом с игроками отображаются таймеры.
Как я считал размер
Минификация исходника — только часть сборки. Бот берёт готовый документ, подставляет остаток векторных данных и проверяет итоговый HTML.
Здесь легко получить красивую цифру, измерив файл до подстановки. Я оставил проверку в коде запуска, чтобы бот сразу сообщил о превышении лимита.
raw = html.encode("utf-8") if len(raw) > 32768 or len(zlib.compress(raw, 9)) > 4096: raise ValueError("HTML-виджет превышает лимит Telegram.")
Для текущей версии получилось так.
Экран |
До сжатия |
После zlib |
|---|---|---|
Лобби |
6 895 байт |
3 395 байт |
Доска |
7 889 байт |
3 957 байт |
Таблица измерена через Python 11 ноября 2026 года, после подстановки векторов. Скрипт сборки на Node.js выводит собственную оценку. При таком небольшом запасе ориентируюсь на проверку Python, которую рекомендует Telegram. Как Telegram считает размер
Теперь даже добавление текста ошибки заканчивается новой сборкой. При этом исходник остаётся читаемым. Сжимается готовый результат, который бот отправляет в Telegram.
Почему доска подвисала при ходе
После оформления оставалась другая проблема. Нажимаешь на клетку, а фигура какое-то время стоит на месте. Особенно заметно это было в партии с компьютером.
Я разделил отображение хода и его подтверждение. Виджет получает от бота список допустимых перемещений. Когда пользователь выбирает фигуру и клетку назначения, интерфейс показывает временную позицию и отправляет запрос.
Бот проверяет игрока, очередь хода, текущую ревизию партии и само перемещение через библиотеку chess. Если ход принят, временная позиция заменяется подтверждённой. Если отклонён — возвращается сохранённая позиция.
Для рокировки пришлось сразу передвигать и ладью. Для взятия на проходе — убирать пешку с соседней клетки. При превращении сначала открывается выбор фигуры, а затем отправляется ход с выбранным типом.
Это улучшило отклик, но перерисовка всей доски всё ещё была лишней работой. Я оставил 64 группы клеток и стал сравнивать состояние каждой с предыдущим. Если фигура, фон и подсветка совпадают, содержимое клетки остаётся прежним.
const key = [fill, letter, target, index].join(); if (cellKeys[slot] === key) continue; cellKeys[slot] = key;
Цикл по клеткам по-прежнему выполняется. Но при обычном ходе в DOM меняются начало и конец перемещения, а также затронутая подсветка.
С компьютером обнаружилась ещё одна задержка. Расчёт начинался в очередном проходе секундного цикла. Я заменил это ожидание на asyncio.Event, а расчёт вынес в asyncio.to_thread.
Перед применением ответа проверяется ревизия партии. Если за время расчёта пользователь уже сдался или позиция изменилась, старый результат пропускается.
Что проверял после переделки
В браузерном стенде я проверял ширины 200, 320, 360 и 420 пикселей. На большом экране меню действий могло вытолкнуть нижние кнопки за границу виджета. Пришлось уменьшить высоту строк игроков и перенести сообщения об ошибках в строку состояния.
Отдельно задерживал ответ на ход и возвращал отказ. Так можно было проверить, что фигура сначала перемещается, а затем корректно возвращается на место. Проверял и специальные ходы — рокировку, взятие на проходе, превращение.
После этого оставалась проверка в самом Telegram. История с конём в шапке как раз показала, зачем она нужна. Браузерные снимки помогали с вёрсткой, но окончательный вид я оценивал по реальному клиенту.
На стороне бота сейчас девять автоматических тестов. В них проверяются партии, восстановление из базы, превращение пешки, права и переключение между экранами.
По результату я сохранил три режима, настройки перед партией, крупную доску и основные действия. Больше всего пришлось пересмотреть устройство интерфейса — разделить лобби и игру, вынести обложку, сократить оформление и обновлять отдельные клетки. Сам лимит в 4 КБ заставлял возвращаться к этим решениям после каждого заметного изменения.

Приглашение другу отправляется отдельной карточкой. После принятия открывается партия в личном чате с ботом.
Попробовать бота можно здесь — @MasterCastleApp.
Исходный код проекта — на GitHub.