🏗️ Архитектура — авторитет и стек

Три решения определяют форму dicechess-play: где исполняется движок, на чём фронтенд, и как партия попадает в аналитику.


1. Где живёт авторитет игры

Решение фазозависимое и сознательное (см. ADR-0002-Klientskiy-avtoritet-do-fazy-3).

  • Фазы 1–2 (бесплатная игра с ботом, аноним): клиент. Движок @rabestro/dicechess-engine (Scala.js, ~1 МБ ESM) в Web Worker считает легальные ходы, применяет их и выбирает ход бота (DiceChess.getBestMove). Кубики — клиентский Math.random(). Легальность пользователя ограничивает chessground (movable.dests из getLegalUciMoves). Это корректно, потому что в бесплатной игре с ботом нечего читерить, а реплей-гейт аналитики (07-Kontrakt-ingest-i-validatsiya-dvizhkom) всё равно проверяет легальность каждой записанной партии.
  • Фаза 3 и далее: сервер. Авторитет переезжает на JVM-jar lv.id.jc:dicechess-engine-scala_3 за новым http4s/WebSocket-сервисом: истинное GameState, серверный RNG с аудируемым seed, валидация каждого микро-хода, порт BotMatchRunner как игровой цикл. Клиентский движок деградирует до «оптимистичного UI» и подсветки легальных ходов.

Тонкий, но корректный срез

Фазы 1–2 — это не выброшенный прототип, а тонкий срез будущей серверной системы. Переход на сервер — эволюция, а не переписывание.


2. Фронтенд-стек

SvelteKit 2 + Svelte 5 (runes) + Tailwind 4 + adapter-static + PWA — тот же стек, что у dicechess-analytics-ui (см. ADR-0004-Frontend-SvelteKit-2-PWA).

  • Play-компоненты lab (ChessgroundBoard, DiceBox, MoveHistory, PlayWithBotView, стор playWithBotStore.svelte.ts) — это уже Svelte 5 runes, переносятся почти 1:1.
  • Тема-система копируется из аналитики как есть (это чистый CSS + один стор, не SvelteKit-специфично): app.css (Tailwind 4 @theme + [data-theme='…'], семь тем — dark/light/dracula/nord/solarized-dark/tokyo-night/gruvbox), стор themeStore.svelte.ts (localStorage['dicechess-theme']), boot-скрипт в app.html (анти-FOUC).
  • PWA через @vite-pwa/sveltekit — устанавливаемое приложение + офлайн-игра с локальным движком (бонус, который у lab фактически уже работает).
  • adapter-static → обычный статический SPA, деплоится на Koyeb static / Cloudflare Pages / aurora Nginx.

3. Топология ингеста

Браузер никогда не держит INGEST_TOKEN. С 2026-08 браузерные отчёты принимает сам play-api (POST /ingest/games, ADR-0016); отдельный Koyeb-шлюз (ADR-0005) выведен из эксплуатации.

flowchart LR
    B["Браузер — SPA dicechess-play<br/>движок Scala.js · IndexedDB outbox"] -->|"POST /ingest/games<br/>без токена"| P["play-api · Oracle<br/>rate-limit · 256KB cap · структурная валидация"]
    P -->|"client_reports · IngestDeliverer<br/>Bearer · POST /api/games"| A["sync.jc.id.lv<br/>POST-only наружу"]
    A --> DB[("analytics · Postgres<br/>aurora")]
  • play-api валидирует отчёт структурно (source=playsite, id = UUID), кладёт в очередь client_reports (отдельную от транзакционного outbox собственных партий — подделываемое не смешивается с доверенным) и доставляет IngestDeliverer-ом с backoff и парковкой 4xx.
  • Авторитетная валидация — по-прежнему движковый реплей в самой аналитике (422); реджект реплея паркуется на сервере play-api.
  • IndexedDB-first outbox в браузере остаётся: упавший POST ретраится при следующем визите, структурный реджект (400/422) карантинится.
  • С фазы 3 ингест уезжает на сервер игры — токен остаётся серверным, браузерное реле уходит.

Пин версии движка ≥ 1.4.3

Клиент, шлюз-валидатор и backend аналитики должны реиграть одной версией движка. Backend на 1.4.3 (DiceChessEngineVersion в build.sbt), lab — на 1.4.2. Рассинхрон версий — главный источник «тихих» 422. Boot-time assert равенства версий — операционный инвариант.


4. Что переиспользуем

БерёмОткудаКак
play-vs-bot UIdicechess-lab/frontend-pwaкомпоненты + стор почти as-is, снимаем App.svelte auth-гейт
тема-системаdicechess-analytics-uiapp.css + themeStore.svelte.ts + boot-скрипт app.html
движок@rabestro/dicechess-engineпин ≥ 1.4.3 под версию backend
типы контрактаobserver/sync types.tsGameIngestWire/PlayerInputWire/TurnInputWire вербатим
клиент ингестаdicechess-sync/src/ingest.tsingestGame(baseUrl, token, payload) вербатим
эталон самосбораdicechess-extension (gameRecorder.js)first-party источник, сам собирает GameIngest из своей партии

Что не переиспользуем: dicechess-bots (STOMP-клиент к dicechess.com, не автономный цикл) и normalizeStateMap из observer/sync (парсит StateMap dicechess.com, которого у нас нет — GameIngestWire строим напрямую).

🔗 Связанное