Skip to content

Adapters & registry ​

The adapter layer turns an incoming event into progression changes. It supports both hand-written Rust and data-driven specs behind one trait.

GameAdapter trait ​

Defined in backend/crates/adapters/src/adapter.rs. An adapter applies a ClientEvent to mutable state within a PlayContext and may emit internal events.

Current two classes (historical D2) ​

ClassSourceWhen
nativeRust adapter (for example match3.rs)Non-standard logic, physics, multi-phase
dataSpecGameAdapter + GameplaySpecTypical streak/score/reveal/spin games

DB-driven registry ​

AdapterRegistry::from_manifests() reads game_templates rows, builds a TemplateManifest per row, and uses adapter_for_manifest() to pick native or SpecGameAdapter. This means the catalog of games is configured in the database, not compiled in.

Current legacy play shell (historical D4) ​

The implemented game UI is currently the widget bundle. GET /v1/play renders one generic shell (routes/play/shell.rs): CSP iframe, theme CSS vars, PlayFlowShell bootstrap, and loads ui_bundle_url from the manifest. Unknown templates without a bundle return 404 — there is no legacy per-game fallback.

Для полноценных игр этот адаптер не расширяется. Рядом живёт второй, независимый контракт — GameMechanic.

GameMechanic и реестр механик ​

Механики игр нового поколения — чистый Rust без БД в backend/crates/adapters/src/: mechanic.rs (контракт), registry.rs (реестры), common/ (общие модули), match3_v1/ (первая механика).

Виды механик и модели доверия ​

Вид (MechanicKind)Модель доверия (AuthorityModel)Где считается
RunStep — сервер выполняет каждую команду; Replay — сервер заново проигрывает присланную лентуGameMechanic
OutcomeOutcome — исход выбирает и бронирует сервис с БДOutcomeRegistry: reward_wheel_v1 (чистые правила R1; клиент ещё не готов)

Дескриптор ​

Общий слой колеса W0 находится в adapters/src/reward_wheel_v1/: строгая таблица6–12секторов, билеты/ресурсы, один утешительный сектор, перенос недоступного веса в него, проценты с точностью0,1% и суммой100%, выбор от системного случайного числа без смещения. Безопасный вид не содержит веса, внутренние причины выбывания и данные выдачи. Иконки пока представлены идентификаторами набора сервиса; HTTP-слой сопоставляет им URL /v1/game-assets/wheel/icons/{id}.svg из закрытого набора10иконок.

Колесо не реализует GameMechanic и не запускается через POST /v1/game-runs. Его client_ready=false, поэтому выпуск оболочки пока не требует отсутствующий чанк. Отдельный слой W1 в api/src/services/wheel/ хранит неизменяемые версии таблиц, выбирает исход с SysRng и коммитит права/проводки/результат/ фрод-флаги в одной транзакции. Адаптер проверяет закрытый исход перед commit; HTTP-клиент не передаёт вес, случайное число или выбранный сектор. Материальные призы не входят в R1. Админская демонстрация движения и симуляция весов не создают GameRun или настоящую награду.

GameDescriptor { game_id, game_version, kind, authority, viewport, client_entry_key, limits, default_level, player_title }:

  • player_title — название механики для игрока (РФ-13; у Match3 — match3_v1::levels::PLAYER_TITLE, добавлено в G1.6); отдаётся как play.title в GET /v1/lobby и в теле 409 mechanic_changed;
  • viewport — только проверка: тест регистрации требует 420×760 оболочки;
  • client_entry_key — ключ манифеста Vite от корня games/ (у Match3 — match3_v1/client/src/index.ts); по нему сервер ищет вход механики в выпуске shell/<release> и в mechanics.json;
  • limits: MechanicLimits { commands_per_minute, starts_per_minute, max_payload_bytes, run_ttl_secs } — лимиты FraudGate по ключу с game_id, предел payload команды и срок партии. При регистрации проверяется max_payload_bytes + 1024 ≤ 8192 (общий потолок тела).
МеханикаКоманд/минСтартов/минpayloadСрок партии
match3_v112010≤ 512 Б30 суток

Трейт ​

МетодСмысл
descriptor()Паспорт механики
levels()Дорожка уровней — структура LevelTrack (данные, а не трейт: так решено в G1.2, отступление от PLATFORM §4.3)
level_preview(level)Цели и ходы уровня для лобби, без поля и seed
start(&StartContext)StartContext { level, seed } — закрытое состояние и безопасный вид
apply(state, command, &CommandContext)Одна смысловая команда; время партии (elapsed_ms, since_last_ms, now_ms) даёт сервис
expire(state, ctx)Собственный исход по сроку; по умолчанию None — сервис ставит abandoned с end_reason = expired
resumable(state)Можно ли продолжить после перезагрузки; по умолчанию true
client_view(state)Только то, что клиенту положено видеть

Терминальный MechanicResult несёт end_reason; MechanicStatus по-прежнему знает только active, won, lost — abandoned (replaced, expired) ставит сервис. IntegritySignal.severity (Weak/Strong) — сила сигнала для FraudGate; как сила влияет на решение о награде — решение №14 в архитектурных решениях.

Дорожка уровней ​

adapters/src/common/catalog.rs разбирает games/<механика>/levels/catalog.v<N>.json: track_version, sequence позиций {id, version, game_version} (позиция с 1) и обязательное правило after_last — cycle с from_position или replay_last. Позиция → (level_id, version, game_version) → registry.get(mechanic, game_version): правила партии берутся из дорожки, а не из config_schema шаблона. Идентификатор уровня — [a-z0-9_-]{1,64}. Замороженные определения и корпус v1 сохраняются. Проверка дорожки сохраняет порядок старых ID и допускает явное обновление версий позиций 2 и 3: score_intro@2 и rocket_intro@2 работают с game_version: 2. Match3 содержит 15 позиций; первая остаётся level-001@1 с правилами v1, позиции 2–15 используют v2. Новые ID позиций 4–15 имеют schema2 и version: 1. after_last: cycle с from_position: 11 повторяет позиции 11–15. common/rng.rs — генератор SplitMix64 (перенесён из match3_v1).

В цикле highest_completed остаётся 15, победа увеличивает cycle_wins, повтор не выдаёт промонаграду. Поражение, повтор команды и восстановление партии счётчик не увеличивают. Добавленная позиция 16 имеет приоритет перед следующим повтором. Миграция 058 сохраняет круг текущей партии в game_runs.cycle_round; API возвращает его как level.cycle_round. progression.cycle_round описывает следующий старт, поэтому после победы значения могут отличаться. Исторические партии сохраняют SQL NULL, без вычисления круга по текущему прогрессу.

Реестры и доступность ​

registries() отдаёт Registries { run: MechanicRegistry, outcome: OutcomeRegistry }; MechanicRegistry — по game_id + game_version, kind_of(game_id) — вид механики; reward_wheel_v1 — идентификатор колеса (вид outcome). Механика недоступна для «Играть» (старт — 503 mechanic_unavailable, лобби — play.available = false), если её нет в реестре, game_templates.selectable = false (аварийное выключение; кэш шаблона — до 30 с), её клиента нет в выпуске (not_built) или версия правил позиции не поддержана. Начатые партии доигрываются по своей версии: сборка без этой версии отвечает 409 run_version_unsupported, а не считает ход по другим правилам.

Первая реализация — match3_v1/: поле, поиск совпадений, специальные элементы, каскады, цели, очки и уровни. Правила — docs/plan/2026-08-16/MATCH3.md. Офлайн-лаборатория backend/crates/game_lab (боты, эталонный корпус) во время игры не запускается. Старый match3.rs остаётся только для match3_lite_v1.

Правила и клиент Match3 v2 ​

Реестр содержит match3_v1@1 и match3_v1@2: один движок с профилем правил (RulesProfile). Старые JSON уровней и эталонный корпус v1 сохраняются. V2 читает оба формата уровней: schema1 с прежним поведением и schema2 с цепью, льдом, дырами, несколькими целями и серверными звёздами. Будущие элементы ящика, камня, ингредиента и мёда пока не допускаются валидатором.

Отказ по цепи (locked) или дыре (hole) не расходует ход и не меняет RNG; поддельная команда даёт слабый сигнал FraudGate. За один шаг снимается не больше одного слоя на клетку. Цепь, существовавшая до ударов шага, защищает лёд до следующего шага даже при снятии последнего слоя цепи. Пределы каскада и регенерации исключают бесконечный подбор поля.

game.v2.json описывает правила v2 с client_ready=true; общий клиентский модуль объявляет supported_game_versions: [1, 2]. Паспорт game.json сохраняет правила v1 для первой позиции и старых партий. При старте сервер сначала выбирает точную пару level_id + version и правила позиции, затем проверяет поддержку клиента. Неподдерживаемая версия возвращает 409 client_outdated до создания партии.

Вид v2 содержит массив goals: победа требует всех целей. У clear поля required и current считают клетки с цепью или льдом, а не число слоёв. Двухслойный блокер даёт одну очищенную клетку после снятия последнего слоя. Звёзды считает сервер только для победы по порогам уровня: view.stars, terminal.data.summary.stars и game_won.data.stars. У schema1, активных и проигранных партий поле отсутствует. Превью лобби содержит ходы и цели, а при наличии метаданных — tier и new_element; поле, seed и подсказка хода в него не входят.

Стабильный старый entry.js выбирает самую новую зарегистрированную версию с готовым клиентом и фактически существующей legacy-сборкой. Регистрация серверного @2 или выпуск нового shell не удаляют работающий legacy-вход @1. Валидаторы Rust/TypeScript проверяют заполняемость каждой отдельно снятой цепи, а размеры сетки ограничивают до выделения памяти. Если гравитация оставила пустую клетку вне дыры, команда завершается invalid_state. Оба паспорта Match3 копируются в Rust-стадию штатной сборки образа до компиляции.

Score reconciliation hook ​

For current score games, apply_level_score_reconciliation(..., allow_client_only) is invoked from both the native match3 adapter and the SpecEngine interpreter (score_evidence on GameplaySpec; default incremental = fail closed), then enforced in play_ingress.rs. See SpecEngine. This reconciles client deltas; it is not deterministic board/physics proof.

Internal & integration documentation