🏗️ Архитектура — авторитет и стек
Три решения определяют форму 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 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 строим напрямую).