---
type: tutorial
title: Построй самостоятельно размещённую игру
summary: Построй бандл игры в песочнице с игровым SDK, размести его сам, наведи виджет на него через game-src и производи детерминированную трассу, которую реплеит сервер.
---

К концу этого руководства у тебя будет настоящая игра Caputchin: JavaScript-бандл, который работает в iframe виджета в песочнице, говорит с хостом через игровой SDK и отчитывается **трассой**, которую сервер может перезапустить. В отличие от [ручного режима](/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. Импортирует функцию `register` из `@caputchin/game-sdk` и регистрирует единственную **игровую фабрику**.
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.

Ещё два факта про мост:

- Первый `pass` погашает раунд; о сбойном или брошенном раунде сигналят, просто **не вызывая** `pass` (на этом мосту нет метода `fail`).
- `bridge.error({ code, message })` для внутреннего сбоя игры (ассет не загрузился, исключение), а не для проигрыша игрока. Он всплывает событием `error` хосту.

После того как хост погашает pass, он отпускает токен виджета, и твой бэкенд [проверяет этот токен](/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 вместо этого;
- внешние запросы к 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): подтверждение токена, который производит pass.
