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

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


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

Решение фазозависимое и сознательное (см. ADR-0002 Клиентский авторитет до фазы 3).

  • Фазы 1–2 (бесплатная игра с ботом, аноним): клиент. Движок @rabestro/dicechess-engine (Scala.js, ~1 МБ ESM) в Web Worker считает легальные ходы, применяет их и выбирает ход бота (DiceChess.getBestMove). Кубики — клиентский Math.random(). Легальность пользователя ограничивает chessground (movable.dests из getLegalUciMoves). Это корректно, потому что в бесплатной игре с ботом нечего читерить, а реплей-гейт аналитики (07 Контракт ingest и валидация движком) всё равно проверяет легальность каждой записанной партии.
  • Фаза 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 Фронтенд 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, поэтому между SPA и аналитикой стоит токен-шлюз (см. ADR-0005 Шлюз ингеста на Koyeb).

flowchart LR
    B["Браузер — SPA dicechess-play<br/>движок Scala.js · IndexedDB outbox"] -->|"POST партия"| G["Шлюз ингеста · Koyeb<br/>держит токен · реплей движком"]
    G -->|"Bearer · POST /api/games"| A["sync.jc.id.lv<br/>POST-only наружу"]
    A --> DB[("analytics · Postgres<br/>aurora")]
  • Шлюз держит Bearer-токен, локально реиграет партию движком (вторые ворота перед общей БД) и форвардит через переиспользованный ingestGame(baseUrl, token, payload) из observer/sync.
  • Аналитика уже выставлена наружу POST-only как sync.jc.id.lv — отдельный публичный эндпоинт не нужен.
  • IndexedDB-first outbox (как в lab): упавший POST ретраится/карантинится, не теряется.
  • С фазы 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 строим напрямую).

🔗 Связанное