🏗️ Архитектура — авторитет и стек
Три решения определяют форму 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 UI | dicechess-lab/frontend-pwa | компоненты + стор почти as-is, снимаем App.svelte auth-гейт |
| тема-система | dicechess-analytics-ui | app.css + themeStore.svelte.ts + boot-скрипт app.html |
| движок | @rabestro/dicechess-engine | пин ≥ 1.4.3 под версию backend |
| типы контракта | observer/sync types.ts | GameIngestWire/PlayerInputWire/TurnInputWire вербатим |
| клиент ингеста | dicechess-sync/src/ingest.ts | ingestGame(baseUrl, token, payload) вербатим |
| эталон самосбора | dicechess-extension (gameRecorder.js) | first-party источник, сам собирает GameIngest из своей партии |
Что не переиспользуем: dicechess-bots (STOMP-клиент к dicechess.com, не автономный цикл) и normalizeStateMap из observer/sync (парсит StateMap dicechess.com, которого у нас нет — GameIngestWire строим напрямую).