---
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` | 服务器 | 玩法配置，**可为空且不透明**：平台从不检视它，而且它在回放时于服务端重新解析，绝非客户端声称。`null` 意味着“用 run 自己的默认”。在 MVP 阶段服务器传 `null`；按站点配置是一个延后的阶段。 |
| `trace` | 客户端 | 玩家客户端发出的那团不透明 blob，只由这个函数解读。平台从不解析或为它定类型；它是原始字节或一个字符串，只受一个大小上限和隔离体 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
```

种子是 `SHA-256(sessionId : gameId : roundIndex)` 的低 128 位，作为四个无符号 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 规范浮点，或 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)：从一个普通的 reducer 产出一个合规的 `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` 自检结果。
