Контракт реплея
Контракт реплея это единственная обязательная поверхность, о которой воспроизводимая игра договаривается с платформой. Он публикуется как @caputchin/replay-contract: намеренно крошечный, свободный от зависимостей пакет (он загружается в запечатанный изолят реплея, так что не тянет ничего). Всё остальное о твоей игре, движок, рендерер, формат трассы, твоё.
Игра, которая поставляет соответствующий run, воспроизводима и потому может ставить ворота на ключе сайта; игра без него всё ещё встраиваема, но только как UX (она показывается как Не воспроизводима). Смотри Выпусти игру в маркетплейс об этом различии.
Функция run
Воспроизводимая игра поставляет JS- или WASM-модуль, экспортирующий одну функцию по имени run:
run(seed, config, trace) -> { passed, score, durationMs }type RunFn<C = unknown> = (
seed: Seed,
config: C | null,
trace: Uint8Array | string,
) => Verdict | Promise<Verdict>| Аргумент | Источник | Значение |
|---|---|---|
seed | Сервер | Пер-раундовый сид. Серверная настройка раунда. |
config | Сервер | Игровой config, nullable и непрозрачный: платформа никогда его не инспектирует, и он перерешается на сервере при реплее, никогда не заявляется клиентом. null означает «используй собственные умолчания run». На MVP сервер передаёт null; пер-сайтовый config это отложенная фаза. |
trace | Клиент | Непрозрачный блоб, который испустил клиент игрока, который интерпретирует только эта функция. Платформа никогда его не парсит и не типизирует; это сырые байты или строка, ограниченные только потолком размера и лимитом CPU изолята. |
Порядок аргументов кодирует модель доверия: seed и config это серверная настройка раунда; trace это ввод игрока. Значения, влияющие на ворота (порог прохождения, жизни), безопасно читать из config, но было бы обходом читать из клиентского trace.
Модуль должен экспортировать функцию под именем run (константа RUN_EXPORT_NAME). Хост реплея вызывает её и ждёт результат (инстанцирование WASM асинхронно при первом вызове).
Сид
type Seed = readonly [number, number, number, number] // 128 bits, 4 u32 words, MSW firstСид это младшие 128 бит SHA-256(sessionId : gameId : roundIndex), несомые как четыре беззнаковых 32-битных слова, старшее слово первым. deriveSeed(sessionId, gameId, roundIndex) пакета вычисляет его; сервер выводит его и при выдаче раунда, и снова при реплее, так что он никогда не едет по проводу как доверенный клиентский ввод.
Критически, сид привязывает трассу к одной сессии, игре и раунду: реплей чужой или более ранней трассы под другим сидом даёт passed: false. Эта привязка и есть то, как защищаются от инъекции трассы и атак повтором, так что ты должен засевать всю случайность из него, чтобы живая игра и серверный реплей совпали.
Вердикт
interface Verdict {
readonly passed: boolean // drives the verification decision
readonly score: number // game-defined, any finite number
readonly durationMs: number // finite, non-negative
}passed это единственное поле, которое ведёт решение captcha; score и durationMs несутся в выданном токене (и будущем табло). parseVerdict(value) пакета валидирует не доверенное возвращаемое значение и трактует некорректный результат как отклонённый раунд, никогда не проходной, так что run, который бросает или возвращает мусор, падает закрыто.
Детерминизм это всё требование
Единственное жёсткое правило контракта: run должна быть чистой и детерминированной на двух рантаймах, браузере игрока и серверном изоляте. Идентичные (seed, config, trace) должны давать идентичный вердикт в обоих. Частые способы это сломать, все ловятся самопроверкой публикации как run-not-conforming:
- чтение
Date.now(),performance.now(),Math.random()или других недетерминированных глобалов; - чтение внешнего состояния (DOM, сеть, хранилище), которое изолят реплея не предоставляет;
- опора на математику с плавающей точкой, которая различается между рантаймами.
Как ты достигаешь детерминизма (фиксированная точка, WASM-спецификационные float или IEEE-754 плюс шим нейтрализации) это твой выбор; платформа лишь хостит реплей. Движковый кит предоставляет готовые детерминированные примитивы, если ты предпочитаешь не катать свои.
Как сервер это использует
Артефакт, который загружает изолят, это твой бандл run, либо живой entry, либо выделенный артефакт run из манифеста.
См. также
- engine-kit: произведи соответствующий
runиз обычного редьюсера, с детерминизмом, обработанным за тебя. - Манифест caputchin.json: объявление артефакта
runи его модулей. - Построй игру для маркетплейса: запись трассы, которую реплеит этот контракт.
- Справочник ошибок публикации: результат самопроверки
run-not-conforming.