---
type: reference
title: Контракт реплея
summary: Обязательный контракт run/seed/verdict, который делает игру воспроизводимой, что функция run получает и возвращает, как сид привязывает трассу к одному раунду и как сервер её реплеит.
---

Контракт реплея это единственная обязательная поверхность, о которой **воспроизводимая** игра договаривается с платформой. Он публикуется как `@caputchin/replay-contract`: намеренно крошечный, свободный от зависимостей пакет (он загружается в запечатанный изолят реплея, так что не тянет ничего). Всё остальное о твоей игре, движок, рендерер, формат трассы, твоё.

Игра, которая поставляет соответствующий `run`, **воспроизводима** и потому может **ставить ворота** на ключе сайта; игра без него всё ещё встраиваема, но только как UX (она показывается как **Не воспроизводима**). Смотри [Выпусти игру в маркетплейс](/docs/marketplace-game-development/overview#replayable-vs-not-replayable) об этом различии.

## Функция `run`

Воспроизводимая игра поставляет JS- или WASM-модуль, экспортирующий одну функцию по имени `run`:

```ts
run(seed, config, trace) -> { passed, score, durationMs }
```

```ts
type RunFn<C = unknown> = (
  seed: Seed,
  config: C | null,
  trace: Uint8Array | string,
) => Verdict | Promise<Verdict>
```

| Аргумент | Источник | Значение |
|---|---|---|
| `seed` | Сервер | Пер-раундовый [сид](#the-seed). Серверная настройка раунда. |
| `config` | Сервер | Игровой config, **nullable и непрозрачный**: платформа никогда его не инспектирует, и он перерешается на сервере при реплее, никогда не заявляется клиентом. `null` означает «используй собственные умолчания run». На MVP сервер передаёт `null`; пер-сайтовый config это отложенная фаза. |
| `trace` | Клиент | Непрозрачный блоб, который испустил клиент игрока, который интерпретирует только эта функция. Платформа никогда его не парсит и не типизирует; это сырые байты или строка, ограниченные только потолком размера и лимитом CPU изолята. |

Порядок аргументов кодирует модель доверия: `seed` и `config` это серверная настройка раунда; `trace` это ввод игрока. **Значения, влияющие на ворота (порог прохождения, жизни), безопасно читать из `config`, но было бы обходом читать из клиентского `trace`.**

Модуль должен экспортировать функцию под именем `run` (константа `RUN_EXPORT_NAME`). Хост реплея вызывает её и ждёт результат (инстанцирование WASM асинхронно при первом вызове).

## Сид [#the-seed]

```ts
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`. Эта привязка и есть то, как защищаются от инъекции трассы и атак повтором, так что ты должен засевать всю случайность из него, чтобы живая игра и серверный реплей совпали.

## Вердикт

```ts
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`, который бросает или возвращает мусор, падает закрыто.

## Детерминизм это всё требование [#determinism-is-the-whole-requirement]

Единственное жёсткое правило контракта: `run` должна быть **чистой и детерминированной на двух рантаймах**, браузере игрока и серверном изоляте. Идентичные `(seed, config, trace)` должны давать идентичный вердикт в обоих. Частые способы это сломать, все ловятся самопроверкой публикации как `run-not-conforming`:

- чтение `Date.now()`, `performance.now()`, `Math.random()` или других недетерминированных глобалов;
- чтение внешнего состояния (DOM, сеть, хранилище), которое изолят реплея не предоставляет;
- опора на математику с плавающей точкой, которая различается между рантаймами.

Как ты достигаешь детерминизма (фиксированная точка, WASM-спецификационные float или IEEE-754 плюс шим нейтрализации) это твой выбор; платформа лишь хостит реплей. [Движковый кит](/docs/marketplace-game-development/engine-kit) предоставляет готовые детерминированные примитивы, если ты предпочитаешь не катать свои.

## Как сервер это использует

```mermaid
sequenceDiagram
  participant P as Браузер игрока
  participant S as Сервер Caputchin
  participant R as Изолят реплея
  S->>S: deriveSeed(sessionId, gameId, round)
  S->>P: выдать раунд с сидом
  P->>P: играть, записывая трассу
  P->>S: отправить трассу при прохождении
  S->>S: перевывести тот же сид
  S->>R: run(seed, config, trace)
  R-->>S: вердикт { passed, score, durationMs }
  S->>S: parseVerdict, решить
```

Артефакт, который загружает изолят, это твой бандл `run`, либо живой `entry`, либо выделенный [артефакт `run`](/docs/marketplace-game-development/the-caputchin-json-manifest#the-run-artifact) из манифеста.

## См. также

- [engine-kit](/docs/marketplace-game-development/engine-kit): произведи соответствующий `run` из обычного редьюсера, с детерминизмом, обработанным за тебя.
- [Манифест caputchin.json](/docs/marketplace-game-development/the-caputchin-json-manifest#the-run-artifact): объявление артефакта `run` и его модулей.
- [Построй игру для маркетплейса](/docs/marketplace-game-development/build-a-marketplace-game): запись трассы, которую реплеит этот контракт.
- [Справочник ошибок публикации](/docs/marketplace-game-development/publish-failed-reference): результат самопроверки `run-not-conforming`.
