🎯 Фаза 1 — аноним vs бот + запись в аналитику

Цель. Незарегистрированный гость открывает play.jc.id.lv, играет полную партию с одним из наших ботов целиком в браузере, и партия попадает в dicechess-analytics как новый источник source='playsite' — с корректными идентичностями гостя и бота, детерминированным id и white-POV результатом, без риска для существующих 140k партий.

Игровой цикл уже реализован клиентски в lab (см. 02 Архитектура — авторитет и стек). Реальная работа фазы 1 — четыре вещи: снять auth-гейт, написать маппер, поднять шлюз, запинить версию движка.

Реализовано и проверено вживую (2026-06-23)

Сайт: dicechess-play.pages.dev (Cloudflare Pages, авто-деплой). Шлюз: dicechess-ingest-gateway на Koyeb. Реальные партии пишутся: source='playsite', guest:<uuidv7> (per-browser) + bot:<algorithm>, UUIDv5-id, white-POV результат, термина­ция из движка. Гоча: при cold-start Koyeb первая партия летит ~12с (ингест асинхронный — ок). Отклонённые партии (400/422) карантинятся в IndexedDB (не ретраятся). Известный edge-case: один POST дал 422 (реплей отверг партию) — повод довести golden-corpus.


Идентичность (как пятый писатель)

Полная схема — в 08 Идентичность, источники и дедупликация. Для playsite:

Сущностьexternal_idplayer_typesource
гость (человек)guest:<uuidv7>per-browser, в localStorageguestplaysite
ботbot:<algorithm> (напр. bot:monte-carlo)botplaysite
  • source='playsite' — новое значение. games.source это VARCHAR(20) без enum/whitelist → backend аналитики не меняется вообще.
  • bot:<algorithm> делим с extension: это те же наши движковые алгоритмы. Происхождение партии различается полем games.source, потерь нет; агрегаты по игроку-боту фильтровать по source.
  • player_type липкий (ставится только при первом insert) → маппер хардкодит guest/bot, чтобы навсегда не промахнуться.
  • id партии: UUIDv5('playsite/game/<clientGameUuid>') — детерминированно, ретраи идемпотентны (first-writer-wins, 200 на дубль), как у beturanga.

guest у playsite ≠ guest у sync

В sync guest означает «эфемерный аккаунт dicechess.com, не краулим». У playsite guest:<uuidv7>наш собственный анонимный игрок, по которому мы хотим копить статистику (per-browser). Разные смыслы, общий player_type; различаются формой external_id (guest: + UUID против имени Guest…).


Маппер: история lab → GameIngestWire

Чистый маппер (~150 строк), строит payload напрямую из per-turn истории lab (НЕ через normalizeStateMap). Поля (snake_case):

  • turns[i]{turn_number, active_color (w/b), dice[] (1..6, декод из 7-го поля DFEN), moves[] (UCI; [] для паса)};
  • initial_fen = стандартный стартовый FEN;
  • resultwhite-POV (1 / -1 / 0), победитель известен точно;
  • termination — из терминального состояния движка (countKings → king_captured), не из догадки UI;
  • white_player/black_player — гость vs бот по схеме выше;
  • mode='classic'; initial_stake_amount и денежные дельты = NULL (свободные партии не должны пачкать stake-аналитику).

Путь ингеста

flowchart LR
    SPA["SPA · запись в IndexedDB<br/>(sync_status pending)"] -->|маппер → GameIngestWire| G["Шлюз · Koyeb<br/>локальный реплей движком"]
    G -->|"Bearer · POST /api/games"| S["sync.jc.id.lv"]
    S --> A[("analytics")]
    G -.->|реджект| Q["карантин в IndexedDB<br/>(ручной разбор)"]

См. топологию и обоснование шлюза в 02 Архитектура — авторитет и стек и ADR-0005 Шлюз ингеста на Koyeb.


Гарантия валидности (0 × 422, 0 загрязнения)

  1. Пин движка ≥ 1.4.3 = версия backend — главный источник тихих 422.
  2. Boot-time assert «client gateway backend version».
  3. Два движковых гейта: локальный реплей на шлюзе + реплей backend.
  4. Golden-corpus в CI: пасы (moves=[]), превращения, en-passant в середине хода, king-capture в середине последовательности — должны реиграться чисто и локально, и на staging.
  5. Идемпотентные UUIDv5 — ретраи безопасны (200 на дубль).
  6. Исход из движка, не из UI — backend доверяет result/termination без перекрёстной проверки.

См. полный разбор причин 422 в 07 Контракт ingest и валидация движком.


Скоуп фазы 1 (чеклист)

  • Форк dicechess-play из dicechess-lab/frontend-pwa; снять App.svelte auth-гейт → рендер для null/guest.
  • Пин @rabestro/dicechess-engine ≥ 1.4.3; движок в Web Worker.
  • Хардкод 5 играбельных ботов (random/checkmate-aware/greedy/aggressive/monte-carlo); НЕ доверять getAvailableBots().
  • Гость: guest:<uuidv7> per-browser в localStorage + restore-code UX.
  • Отключить ставки/кошелёк/x2/удвоение для гостей; classic-only, stake = NULL.
  • Маппер history → GameIngestWire (+ исход из терминала движка).
  • Токен-шлюз на Koyeb с локальным реплеем; форвард на sync.jc.id.lv.
  • IndexedDB-first outbox (retry/quarantine) + golden-corpus.
  • Деплой SPA (adapter-static) за play.jc.id.lv.

Фишер с рокировкой запрещён

Движок хардкодит поля рокировки (e1/h1/a1) и не поддерживает Chess960 → партия Фишера с рокировкой отлетит 422 на реплее. Режим Фишера выключаем на всех фазах.

🔗 Связанное