🔬 Ревью бот-платформы (2026-07) — блокеры и план развития
Что это
Архитектурное ревью четырёх репозиториев (
dicechess-engine-scala,dicechess-play-api,dicechess-play,dicechess-reference-bot) через призму видения: платформа, где боты разных команд — на разных языках, с закрытым кодом — играют друг с другом и с людьми, в том числе с бесплатного облачного хостинга (Azure Functions). Развивает 05 Бот-платформа — анонимный Bot API (дизайн) от дизайна к конкретным находкам в коде: что уже есть, что блокирует, в каком порядке строить. Каждое утверждение проверено по коду (файл:строка).
Главный вывод. Платформа ближе к видению, чем кажется: Bot API уже сейчас language-agnostic (REST + ndjson, играбельно из curl, docs/bot-api.md — документация внешнего качества), а GameRoom транспорт-агностичен — webhook-адаптер для serverless добавляется без изменения игровой логики. Между «наши боты dogfood-ят API» и «внешние команды шлют приватных ботов» стоят четыре блокера (§3) и одна юридическая проблема — AGPL (§4). Идея Azure Functions реализуема, но не на текущем стрим-протоколе — нужен webhook turn-push (§5), что подтверждает и уточняет п.8 «лесенки» из заметки 05.
1. Что уже есть (и это сильные козыри)
| Актив | Где | Почему это важно для платформы |
|---|---|---|
| Language-agnostic Bot API | play-api BotRoutes.scala, docs/bot-api.md | Bearer + REST + два ndjson-стрима, WebSocket боту не нужен. Полный цикл играбелен из curl — SDK не обязателен |
| Zero-registration онбординг | POST /bot/anon (30/ч на IP, TTL 24ч) | «Попробовать за 2 минуты» уже работает (ADR-0010 реализован) |
| Транспорт-агностичное ядро | GameRoom.scala — только subscribe/submit/start | Человеческий WS, бот-ndjson и in-process тесты — тонкие адаптеры. Webhook станет третьим, без правок логики |
| Provably-fair кости | DiceSource.scala, рецепт в bot-api.md | Commit-reveal + клиентская энтропия + length-prefixed HMAC + rejection sampling — проверяемо на любом языке (06 Честность кубиков — провабли-фейр и верификация) |
| Серверные часы | GameRoom.deadlineFor | Единственный таймкипер — медленный бот проигрывает по времени, а не вешает платформу. Валидация хода не тарифицируется (клок до receivedAt) |
| Persist-before-broadcast + resume | GameRoom.emit, GameRegistry.resume | «Зафиксированный бросок не может раз-броситься»; партии переживают рестарт сервера |
| Спектатор-режим в клиенте | liveApi.ts (tokenless WS), live/[id] | Полностью работает — не хватает только списка активных игр, чтобы стать «Watch»-страницей |
| Зеркальные кости для турниров | DiceSource.scala:13-17 | Position-independent броски сознательно спроектированы под duplicate-пары — уникальный турнирный формат (§6, ADR-0009) |
Две неожиданно приятные находки для serverless-сценария:
- У ботов нет heartbeat-обязанности. 25-секундные keep-alive — это сервер→клиент (против 60s read-idle Ember); бот не обязан ничего слать периодически.
- У ботов нет forfeit-on-disconnect. 30s reconnect-grace вешается только на человеческий WS (
PlayRoutes.scala:104берётroom.connection, бот-стрим вBotRoutes.scala:106-109— нет). Бот может вообще не держать стрим и играть чистым поллингом через публичныйGET /games/{id}.
2. Карта протокола (для справки)
Полный цикл бота: POST /bot/anon → GET /bot/stream/event (ndjson: ChallengeReceived/ChallengeDeclined/GameStart) → POST /bot/challenge или .../accept → POST /bot/game/{id}/seed (гейт 5s) → GET /bot/game/stream/{id} (первый кадр всегда Snapshot, дальше DiceRolled/TurnPlayed/Rejected/GameEnded) → на свой DiceRolled — POST /bot/game/{id}/move (202, результат асинхронно на стриме). Доставка at-least-once, дедуп по монотонной версии v; отставший на 256 событий подписчик отбрасывается. Терминальное событие закрывает стрим.
Кодировка — Circe discriminated objects ({"Win":{"side":"White"}}, {"Unlimited":{}}), задокументирована в bot-api.md, но машиночитаемой схемы нет — и дрейф ручных зеркал уже случился дважды: Protocol.scala reference-bot’а потерял commit/seed/clientSeeds, а liveTypes.ts клиента заморожен с пометкой «do not clean up».
3. Четыре блокера до «внешних команд»
Это и есть ядро плана
Всё остальное (рейтинги, турниры, TV) строится поверх этих четырёх. Они же — самые дешёвые из крупных работ.
- Легальные ходы не на wire. Сервер уже вычисляет все легальные пути каждый ход (
GameRoom.beginTurn:344→EngineOps.legalMovePaths) — и выбрасывает.bot-api.md:63честно говорит «compute legal moves using the engine», но движок существует только как Scala-артефакт за авторизованным GitHub Packages. Пока ходов нет вDiceRolled/GET /bot/game/{id}/moves, каждая Python/Go-команда обязана реимплементировать правила (включая Maximum Micro-moves Rule — самое сложное). Фикс — поле в Circe + один роут; заодно растворяет AGPL-проблему (§4). - Discovery только live-стримом.
BotEvents— live-only без replay (BotEvents.scala:9-13): пропущенныйChallengeReceived/GameStartтеряется навсегда; нетGET /bot/challenges, нетGET /bot/games, нет webhook. Это убивает serverless (спящий бот не узнает о работе) и калечит обычных ботов после обрыва стрима. Плюс pending-челленджи без TTL и без валидации цели — вызов офлайн-боту висит вечно. - Нет durable self-service identity. Стабильный токен = оператор правит
PLAY_BOT_TOKENSи редеплоит. Anon-токены — in-memory, 24ч; умирают на рестарте сервера, а партии персистятся и резюмятся → после каждого деплоя все живые партии anon-ботов гарантированно заканчиваются timeout-форфейтом. Нужна таблица ботов в существующей play-схеме (Flyway V3): регистрация, ротация, отзыв, зарезервированная колонка webhook-URL. - Human-vs-bot не существует. Челленджи — только bot↔bot (
BotRoutes.scala:78-83), lobby seeks — только guests (LobbyRoutes.scala:47,70),POST /games— два Guest. Центральный пункт видения — «боты играют с людьми и создают движение» — не имеет ни одного серверного роута. Мост: пустить авторизованных ботов в seek-лобби + расширитьSeekполями{name, kind: 'bot'|'human'}, чтобы человек мог фильтровать ботов, а доска показывала «house greedy (bot)» вместо «Opponent».
4. AGPL и приватные боты
Игра по сети обязательств не накладывает; линковка движка (Maven/npm/WASM) — copyleft. Проблема в том, что единственный сегодня путь к корректным правилам — именно линковка, а шаблон «fork it, replace one method» (reference-bot) сам под AGPL-3.0. Серьёзная команда с юристами приватного бота на этом не построит.
Решение из трёх частей:
- (а) легальные ходы на wire (§3.1) — движок ботам вообще перестаёт быть нужен;
- (б) MIT/Apache старт-киты на Python и TypeScript (~150 строк транспорта — curl-цикл в доке это доказывает) с исправленными паттернами: настоящий reconnect с backoff, логирование HTTP-статусов и
Retry-After, re-mint anon-токена на 401, re-challenge наGameEnded(цикл активности!); - (в) страница LICENSING-FOR-BOTS с явной границей wire/линковка. Копирайт наш — при желании возможен dual-license движка.
Reference-bot как шаблон сейчас учит плохому
README заявляет reconnect, которого нет (только
restart: always);fireUnaryглотает все HTTP-статусы (бот с отозванным токеном выглядит живым и молчит); бот считает ход на каждый бросок, включая чужие (на wire нетyourSeat→ 2× compute и трафика);BOT_CHALLENGEстреляет один раз. Всё это унаследует каждый форк — чинить до открытия наружу.
5. Serverless / Azure — вердикт
Текущий стрим-протокол на Azure Functions нежизнеспособен: HTTP-триггер обязан ответить за 230 с, выполнение ≤10 мин (Consumption), входящие по удерживаемому сокету функцию не триггерят. Это подтверждает интуицию п.8 «лесенки» (заметка 05, §6) — «смена транспорта». Уточнения по свежим лимитам (2026):
| Хостинг | Бесплатно | Текущий протокол | Webhook-протокол |
|---|---|---|---|
| Azure Functions Flex Consumption | 250k выполнений + 100k ГБ·с/мес | ❌ | ✅ рекомендация №1: партия ≈ 40 вызовов → тысячи партий/мес |
| Azure Functions Consumption (legacy, retire 2028) | 1M выполнений/мес | ❌ | ✅ |
| Azure Container Apps | ≈200 ч активной мин-реплики/мес (180k vCPU·с), scale-to-zero | ✅ WS as-is + wake-хук | ✅ — сюда JVM/Scala-ботов |
| Cloudflare Durable Objects | 100k req/день | ⚠️ исходящий WS держит объект ≤15 мин — хватает на короткую партию; только JS/WASM | ✅ |
| Oracle Always Free VM | 24/7 VPS (квоты урезают) | ✅ с оговорками | ✅ |
| Fly.io / Railway | больше не бесплатны | — | — |
| Render Free | усыпляет без входящего трафика → сокет-бот вылетает | ❌ | ⚠️ |
| GitHub Actions | запрещено ToS (serverless-use) | ❌ | ❌ |
Webhook turn-push (ключевое протокольное дополнение): зарегистрированный бот привязывает callback-URL + HMAC-секрет; сервер на его ход POST-ит {gameId, dfen, dice, clocks, turnToken}; бот отвечает ходом синхронно в теле ответа или асинхронно идемпотентным POST с turnToken; retry/backoff, N подряд таймаутов = forfeit. Cold start 1–10 с лечится lag-allowance (плюминг есть — клок и так считается до receivedAt) либо минимальным контролем Fischer 3+2. Архитектурно — третий тонкий адаптер над GameRoom; dispatcher переиспользует outbox/backoff-машинерию IngestDeliverer. Создание игр из cron-триггера через REST закрывает «боты сами создают движение».
Работает уже сегодня без изменений сервера: одна инвокация функции минтит токен → челленджит house-бота (accept синхронный, gameId в ответе) → играет партию до конца поллингом GET /games/{id}. Ограничение — только против always-on оппонента и в рамках одной инвокации.
6. Находки по репозиториям
dicechess-play-api — самый сильный актив, но открывать наружу рано
Риски до открытия (всё проверено по коду):
- DoS через спам ходов: каждый
SubmitTurnзапускает полный перебор легальных путей (EngineOps.findLegalPath), rate limit есть только на/bot/anon. - Write-amplification: каждый emit — включая
Rejected! — персистит полный JSONB-снапшот партии (GameRoom.emit:299-307) → спам-бот превращает мусорные запросы в upsert-ы на изношенный SSD aurora. - Challenge-спам:
Challenges.createбез TTL, капов и проверки цели — map растёт вечно (у Lobby sweeper есть, у Challenges нет). - Загрязнение корпуса: неаутентифицированный
POST /games+ force-start через 5 с + timeout через 120 с → мусор уезжает в аналитику (исключается толькоAborted). - Валидация TimeControl:
SuddenDeath(-5)/PerMove(0)создают instant-forfeit игры. - Мелочи: README врёт «in-memory for now» (persistence смёржен), compose не отражает Postgres-режим,
Protocol.scala:22-26врёт «клоки не enforced».
dicechess-reference-bot — правильная идея, обманчивое исполнение
См. callout в §4. Плюс: пиннинг движка 1.6.1 против независимо обновляемого сервера = массовые Rejected при legality-расхождении (прецедент — ep-канонизация 1.6.1). Ценный актив: пиннед-JSON в ProtocolSuite.scala — готовые language-neutral протокольные фикстуры, надо только вынести из Scala-теста в .json-файлы.
dicechess-engine-scala — спек-качества ядро за стеной
- GitHub Packages требует токен даже на чтение — первая стена для любого внешнего автора (и для free-tier облачных сборок).
- WASM-пакет — это Scala.js-WASM с JS-загрузчиком, не WASI: из Python/Go не загрузить.
- Конформанс-фикстуры (
movegen_{1,2,3}_dice.json,perft_suite.json: DFEN → ожидаемые UCI-ходы) — ровно то, что нужно реимплементаторам — похоронены в test resources; опубликовать release-ассетами. estimateEquityпринимаетrollouts/maxPliesбез верхнего капа — DoS-вектор для любого будущего сервис-враппера.- Дрейф: committed
dist/(1.2.4) иdist-wasm/(1.6.0) против 1.6.2-SNAPSHOT; рукописныйd.tsбезgetAvailableBots/clock.
dicechess-play — витрина почти готова, но выключена
- Спектатор работает end-to-end, недостижим без списка игр → Watch-страница = самый дешёвый видимый шаг (нужен только
GET /gamesна сервере). - Боты невидимы как боты; входа «сыграть с серверным ботом» нет (карточка «Play a bot» — только шесть браузерных).
- Live-партии не сохраняются в историю/
/me(человек, проигравший внешнему боту, не может посмотреть партию). commitкостей приходит вCreateGameResponseи выбрасывается — fairness невидим (фоллоу-апы в PR#30, см. 06 Честность кубиков — провабли-фейр и верификация).- Клиентский vs-bot путь с
Math.random-костями — готовый вектор отравления будущих лидербордов → нужен trust-tier в ingest-контракте (server-authoritative vs client-reported). - Seat-токены в query string (утечка через history/скриншоты) → перенести во fragment.
7. План развития
Соответствие «лесенке» из заметки 05 §8 сохранено (номера в скобках); ревью уточняет содержимое и порядок внутри пунктов.
flowchart TB H["Этап 0 · Hardening<br/>rate limits · TTL челленджей ·<br/>skip Rejected-persist · trust-tier ингеста"] F["Этап 1 · Фундамент DX<br/>legal moves на wire · polling-REST ·<br/>durable bot registry"] V["Этап 2 · Видимость и активность<br/>Watch/TV · activity-loop ·<br/>MIT старт-киты · sandbox-in-a-box"] E["Этап 3 · Экосистема<br/>боты в лобби (HvB) · Glicko-2 ·<br/>профили и directory"] K["Этап 4 · Киллер-фичи<br/>webhook turn-push · mirrored-dice турниры ·<br/>лиги с боссами · resource classes"] H --> F --> V --> E --> K
Этап 0 — Hardening (гейт для всего; S-effort, паттерны уже в коде)
- Обобщить
AnonMintLimiter(он уже generic) на per-token лимиты/bot/challengeи команд в партии; per-IP наPOST /gamesи seeks. - TTL-sweep + валидация цели для челленджей (зеркало существующего
Lobby.sweep); кап pending на бота. - Не персистить снапшот на
Rejected; валидировать границы TimeControl. - Trust-tier в ingest (server-authoritative-commit-reveal vs client-reported) — согласованно в трёх ручных копиях контракта; карантин мусорных сигнатур (0 ходов одной стороны, sub-N-ply timeout). Защитить корпус до прихода бот-трафика.
- Адъюдикация: партия, где сторона не сделала ни хода, —
Aborted, неTimeout(мусор не рейтингуется и не уезжает в корпус).
Этап 1 — Фундамент DX (три блокера из §3; лесенка п.4→5)
- Legal moves на wire: списки UCI-путей в
DiceRolled+GET /bot/game/{id}/moves(сервер уже всё считает). - Polling-полный REST:
GET /bot/challenges,GET /bot/games, синхронная причина реджекта (409/422 тело вместо вечного 202) — заодно путь resume после рестарта для самого reference-bot. - Durable bot registry: таблица в play-схеме, self-service регистрация/ротация/отзыв, дешёвая верификация владения (GitHub-линк или magic link), зарезервированный webhook-URL. Фиксит форфейты после деплоя.
- Версионировать протокол:
/v1или поле версии, OpenAPI + JSON Schema, фикстуры изProtocolSuite— release-ассетами.
Этап 2 — Видимость и активность (лесенка п.3)
- Watch + Dice Chess TV:
GET /games(фильтр поGameRegistry) +/watch-роут по паттерну/lobby; TV-режим авто-следует топовой партии. Витринный эффект максимальный при S-effort. - Activity-loop в reference-bot: re-challenge на
GameEndedc задержкой + Semaphore-кап параллельных игр. - MIT старт-киты Python/TS (template-репо, «Use this template») + фикс паттернов Scala-шаблона.
- Sandbox-in-a-box: docker-compose из готовых ghcr-образов (play-api умеет in-memory без Postgres) — команды перестают тестировать в проде.
- Легальный движок без стен: зеркало npm-пакета на публичный npmjs, конформанс-фикстуры release-ассетами, неинтерактивный line-режим CLI (
echo 'legal <dfen>' | dicechess) как subprocess-оракул для любого языка.
Этап 3 — Экосистема (лесенка п.5–6)
- Боты в human-лобби: авторизованные боты постят/принимают seeks;
Seek + {name, kind}; вход «Play an online bot» в клиенте. Idle house-боты в лобби решают проблему пустого лобби. - Glicko-2 в analytics офлайн-батчем (ноль нагрузки на игровой JVM): рейтингуются только server-authoritative партии зарегистрированных identity; anon и same-team пары — нет (закрывает фарм марионетками); версии ботов (
#v2) — отдельные рейтинговые сущности; месячные сезоны. - Профили ботов, bot directory (
GET /bots: online-статус изBotEvents.subscribers, предпочтения по контролю, auto-accept флаг), бейджи. - Live-партии в историю клиента; commit + «verify this game» на доске.
Этап 4 — Киллер-фичи (лесенка п.7–8)
- Webhook turn-push (§5) + «Deploy to Azure»-кнопка в Python-старт-ките = онбординг внешней команды за один вечер.
- Mirrored-dice турниры: duplicate-пары (одинаковая последовательность костей, цвета наоборот, счёт парой) — нейтрализация удачи, формата нет ни у кого.
TimedArenaRunnerдвижка уже прототипирует пейринг и скоринг офлайн. - Лиги с боссами (CodinGame-модель): шесть difficulty-ботов — готовые боссы; «мой Python-бот обыграл greedy-v2» — конкретный майлстоун для команды.
- Resource classes (serverless / standard / unbounded): lag-allowance с клока, минимальный контроль, сегментированные пулы рейтинга — асимметрия compute из проблемы честности становится фичей.
- Пост-геймовый аудит:
GET /bot/game/{id}/recordиз уже персистируемого JSONB (все события,receivedAt, причины реджектов, commit/seed/clientSeeds) — фундамент споров и целостности рейтинга. - Метрики: Prometheus из play-api (комнаты, стримы, rejected-rate, outbox depth, latency перебора) + Grafana на dexus/rpi4; публичные health-карточки ботов («последнее подключение, доля реджектов») — команды сами диагностируют «глухих» ботов.
Лесенка п.9 (submit-and-run песочница) — без изменений: после обкатки connect-in.
🔗 Связанное
- 05 Бот-платформа — анонимный Bot API (дизайн) — дизайн, который это ревью проверяет кодом; лесенка милстоунов.
- 01 Дорожная карта · 04 Фаза 3 — Human-vs-Human (дизайн) · 06 Честность кубиков — провабли-фейр и верификация
- ADR-0009 Бот-API и турниры · ADR-0010 Анонимные self-service боты · ADR-0008 Честность кубиков — server CSPRNG + commit-reveal
- Публичный контракт: bot-api.md · стартер: dicechess-reference-bot
- Bots — алгоритмы house-ботов, турнирный журнал.