---
type: tutorial
title: 构建一个自托管游戏
summary: 用游戏 SDK 构建一个沙箱化的游戏包、自己托管它、用 game-src 把组件指向它，并产出服务器重放的那条确定性轨迹。
---

到这篇教程结束时，你将有一个真正的 Caputchin 游戏：一个跑在组件沙箱化 iframe 里、通过游戏 SDK 和宿主对话、并报告一条服务器能重跑的 **轨迹** 的 JavaScript 包。不像 [手动模式](/docs/custom-game-development/manual-mode)，一个自托管游戏能 [给一个站点密钥设关卡](/docs/site-keys/games)，因为它的回合可回放。这篇教程讲构建并托管这个游戏；[回放与设关卡](/docs/custom-game-development/replay-and-gating) 讲那个真正把关卡打开的工件。

这适配在哪里，见 [跑你自己的游戏](/docs/custom-game-development/overview)。你需要一个托管一个静态 JavaScript 文件的地方（任何 CDN 或静态主机），以及你页面上的组件。

## 一个自托管游戏的形状

一个自托管游戏是一个自包含的 JavaScript 包，它：

1. 导入 `@caputchin/game-sdk` 的 `register` 函数，并注册单一一个 **游戏工厂**。
2. 渲染进组件交给工厂的那个容器，用随它一起传来的解析出的语言、皮肤和配置。
3. 在逐回合的种子下确定性地玩出来，把这次游玩记录成一条 **轨迹**。
4. 在一次获胜时，带着那条记录调用 `bridge.pass({ trace })`。

组件把你的包加载进它的 iframe 并调用你的工厂；工厂被调用 *就是* 那个开始信号（没有一个单独的开始事件要等）。

## 1. 注册一个游戏工厂

安装 SDK，并带着你的工厂调用 `register` 一次。你 **不** 传清单；服务器解析预设并把它们交给工厂。

```js
import { register } from "@caputchin/game-sdk";

register((container, bridge, ctx) => {
  // container — a DOM element inside the sandboxed iframe; render into it.
  // bridge    — push-only channel to the host (pass / error / setSize).
  // ctx       — the per-round context (seed + resolved locale/skin/config).

  const cleanup = startGame(container, bridge, ctx);

  // Return an optional cleanup function; the widget calls it on unmount.
  return cleanup;
});
```

```bash
npm install @caputchin/game-sdk
```

## 2. 读取逐回合上下文

工厂的第三个参数，`ctx`，携带你游戏为这位访客所需的一切：

- `ctx.seed`：逐回合的回放种子。**从这里派生所有随机性。** 当游戏在一个已核验会话之外跑时为 null。
- `ctx.locale`：解析出的语言对象：`ctx.locale._lang` 加上你已翻译的字符串键。
- `ctx.skin`：解析出的皮肤对象：`ctx.skin._theme`（始终是 `light` 或 `dark`）加上你的颜色和资源键。
- `ctx.config`：解析出的玩法配置（或 null）。

这些是为这位访客 **解析出的预设**，由 Caputchin 从你仪表盘定义的 [模式与预设](/docs/custom-game-development/dashboard-schema) 产出。你从它们上面读你自己的键；你自己不解析任何东西。

如果你的布局需要一个明确的尺寸，就在你第一次绘制之后告诉宿主一次：

```js
bridge.setSize(360, 480);
```

## 3. 让游玩确定性

这是那条让一个游戏可设关卡的规则：**给定种子和玩家的输入，游戏必须确定性。** 不要为任何影响结果的东西调用 `Math.random()` 或读墙上时钟；从 `ctx.seed` 派生每一个随机选择。服务器在同一个种子和同一条记录下来的轨迹下重跑你的逻辑，并且必须得到同一个裁定。

```js
// A tiny seeded PRNG; feed it ctx.seed so the server reproduces the run.
function makeRng(seed) {
  let s = hashToInt(seed);
  return () => (s = (s * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
}
```

在你游玩时，把驱动结果的那些输入（玩家按顺序击中了哪些目标，如果时机要紧也带上时机）记录下来。把那条记录序列化成一个字符串：那个字符串就是你的轨迹。

## 4. 带着轨迹通过

当访客获胜时，把轨迹交给宿主。`pass` 接受一个带单一一个 `trace` 字符串的对象：

```js
function onWin(traceString) {
  bridge.pass({ trace: traceString });
}
```

这条轨迹 **对平台不透明**：它就是一个只由你的游戏定义的字符串，使得你的逻辑、和种子结合起来，重现这个结果。游戏在这里不报告一个分数；计分，如果有的话，是你自己在 iframe 内的事。

再两个 bridge 事实：

- 第一次 `pass` 兑现这个回合；一个失败或被放弃的回合，靠干脆 **不调用** `pass` 来示意（这个 bridge 上没有 `fail` 方法）。
- `bridge.error({ code, message })` 是给一个游戏内部失败的（一个资源加载失败、一个异常），而非一次玩家落败。它向宿主浮现一个 `error` 事件。

在宿主兑现这次通过之后，它释放组件令牌，而你的后端一如往常 [核验那个令牌](/docs/integration-guides/verify-on-your-backend)。

## 5. 为沙箱打包

组件恰好加载一个脚本 URL，所以一切都必须在那单一个文件里。把你的打包器（esbuild、rollup、vite、webpack；它们中任何一个）配置为一个自包含的产物：

- 把资源内联成 data URL（精灵图、声音、字体）；
- 禁用代码拆分；
- 如果你用 WASM，把它作为 base64 嵌入，并从字节实例化。

那些在沙箱里面 **不** 起作用的模式，因为这个 iframe 是不透明来源、带一个严格的 CSP：

- 路径相对的 `fetch('./sprite.png')`：没有一个可供拉取的路径；
- 动态的 `import('./chunk.js')`：那第二个 URL 被挡掉；
- `new Worker('./worker.js')`：改从内联的 `Blob` URL 生成 worker；
- 运行时的外部 CDN 拉取：`connect-src` 把它们挡掉。

## 6. 托管这个包并把组件指向它

把构建出的文件通过 `https` 从你自己的静态主机提供（本地开发允许回环 `http`），然后把组件指向它：

```html
<caputchin-game
  sitekey="cpt_pub_..."
  game-src="https://cdn.example.com/my-game/game.js"
></caputchin-game>
```

组件把你的包加载进它沙箱化的 iframe、带着上下文调用你的工厂、并等你的 `pass`。在这一刻游戏跑并验证，但它还不被允许给一个密钥 **设关卡**。

## 7. 让它可设关卡

要把你的自托管游戏用作一个验证关卡，还需要两样东西，接下来都讲到：

1. **在仪表盘上把它注册为一个自定义游戏**（一个你选的 id），并定义它的字段 [模式与预设](/docs/custom-game-development/dashboard-schema)，好让上下文携带真实的 locale/skin/config。
2. **上传一个回放工件**：一个你游戏逻辑的 headless 构建，服务器跑它来从种子和轨迹重新推导那个裁定。见 [回放与设关卡](/docs/custom-game-development/replay-and-gating)。

在那个回放工件被上传并通过它的自检之前，这个自定义游戏显示 **不可回放**，且无法设关卡。

## 另见

- [回放与设关卡](/docs/custom-game-development/replay-and-gating)：那个把关卡打开的工件。
- [仪表盘模式参考](/docs/custom-game-development/dashboard-schema)：定义上下文所解析的那些字段。
- [手动模式](/docs/custom-game-development/manual-mode)：那个更轻的、不设关卡的替代方案。
- [在你的后端核验](/docs/integration-guides/verify-on-your-backend)：确认一次通过产出的那个令牌。
