# @playbox-ai/playable-playtest

> Read-only game state snapshots for AI playtesting

Latest version **0.1.0** (published 2026-09-23) · UNLICENSED license · 0 weekly downloads

## Install

```sh
npm install @playbox-ai/playable-playtest
pnpm add @playbox-ai/playable-playtest
yarn add @playbox-ai/playable-playtest
bun add @playbox-ai/playable-playtest
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.1.0 |
| Published | 2026-09-23 |
| First published | 2026-09-23 |
| Weekly downloads | 0 |
| License | UNLICENSED |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 0 |
| Unpacked size | 40.4 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | b-kirill, pavelsamoylenko |

## Links

- npm: https://www.npmjs.com/package/@playbox-ai/playable-playtest
- npm.io page: https://npm.io/package/@playbox-ai/playable-playtest

## Recent versions

- 0.1.0 (latest) — 2026-09-23

## README

# Playable Playtest

`@playbox-ai/playable-playtest` — состояние игры для AI-playtest и debugging.
Независим от Playable Kit, Settings и движка. ESM, TypeScript declarations,
без runtime-зависимостей. Совместимость с Cocos предназначена для Creator 3.x Web.

Установка: `npm install --save-exact @playbox-ai/playable-playtest@0.1.0`.
Для локальной разработки: `npm run build`, затем `npm pack` и установка `.tgz`.
Подключение к игровому bootstrap и внешней оболочке выполняется отдельно.

```ts
import { createPlaytest } from '@playbox-ai/playable-playtest';
import { attachPlaytestPreview } from '@playbox-ai/playable-playtest/preview';

// enabled и buildId передаёт ваш bootstrap. Универсального флага сборки нет.
const playtest = createPlaytest({ enabled: previewEnabled, buildId, runId: 'round-1' });
const unregister = playtest.exposeState({
  description: 'phase: playing/won/lost; position: world units; timeRemaining: seconds.',
  isReady: () => game.ready,
  read: () => ({
    phase: game.phase,
    player: { position: { x: game.player.x, y: game.player.y } },
    timeRemaining: game.timeRemaining,
    objective: { collected: game.collected, required: game.required },
  }),
});
const detach = attachPlaytestPreview(playtest);
// При реальном рестарте: game.ready = false; playtest.startRun('round-2');
// Затем создать/сбросить раунд и выставить game.ready = true.
// При teardown: detach(); unregister(); playtest.dispose();
```

`exposeState()` регистрирует один provider на экземпляр. `read()` вызывается
только при запросе снимка, синхронно, без изменения игры. `isReady` необязателен;
без него зарегистрированный provider считается готовым. Async read не поддержан.
На этапе загрузки/рестарта/смены сцены возвращай `false` из `isReady`.
Игровые ошибки не подавляй; ошибка чтения становится `read_failed`.

## Получение из оболочки

```ts
import { createPlaytestHost } from '@playbox-ai/playable-playtest/host';

// После iframe load; не импортировать /host в игру.
const host = createPlaytestHost(iframe);
const snapshot = await host.getSnapshot();
// Browser automation может обращаться к host через явно выставленный
// оболочкой window.playtestHost. Пакет сам глобальные переменные не создаёт.
host.dispose();
```

Результат: `{ buildId, runId, schemaVersion, sequence, capturedAt, description, state }`.
`capturedAt` — Unix ms начала чтения; `sequence` растёт после каждого успешного
снимка в течение жизни экземпляра. Это номер снимка, не номер игрового кадра.
Если нужна привязка к кадру/симуляции, добавь `tick` в состояние игры.
`runId` должен быть уникален для каждого раунда, включая Reload страницы;
примеры ниже используют ID сессии и счётчик. `buildId` идентифицирует весь билд,
не только defaults. Старый снимок после действия не подтверждает его результат.

## Контракт состояния

`schemaVersion` задаётся в `exposeState`, положительное целое, по умолчанию 1.
Повышай его при несовместимой смене формы или смысла state. Он независим от
версии протокола доступа (1) и идентификатора билда.

- Только выбранная разработчиком проекция: обычный JSON-объект, вложенные
  объекты/плотные массивы, строки, boolean, конечные числа и null.
- Не возвращать Cocos Node/Vec3, Three.Object3D, DOM, Map, Date, функции,
  undefined, bigint, NaN/Infinity, циклы, accessors или Promise. Копируй
  координаты явно: `{ x: node.position.x, y: node.position.y }`.
- Снимок отделён от объектов игры; изменение полученной копии не меняет игру.
- Ограничения: 64 KiB UTF-8 JSON состояния, глубина 32, 10 000 узлов;
  description до 4096 символов, ID до 128. Большие списки агрегируй/ограничивай
  и показывай общее количество и факт усечения.
- Снимок согласован в пределах синхронного чтения JS. Состояние в workers или
  других потоках игра сама публикует в стабильную проекцию перед чтением.
- Не добавляй скрытую информацию в обычный AI-playtest. Если debug-снимок
  раскрывает скрытые цели/врагов, явно пометь это в description и отчёте теста.

## Протокол v1

Родитель → iframe:
`{ channel: 'plbx:playtest', version: 1, kind: 'getSnapshot', requestId }`.

Ответ → родитель:
`{ channel, version: 1, kind: 'snapshot', requestId, ok: true, snapshot }`
или `{ channel, version: 1, kind: 'snapshot', requestId, ok: false, error: { code, message } }`.

Протокол только читает; restore, set, ввод и debug-команды в него не входят.
Iframe принимает запросы только своего непосредственного родителя, host —
ответы только своего iframe и ожидаемого requestId. Домен не зафиксирован;
включённый preview разрешает чтение своему родителю на любом origin, в том
числе sandbox с opaque origin. Не включай его для рекламных размещений.
Это явный opt-in, не способ защитить секреты внутри браузерной игры.

Таймаут host по умолчанию 2000 ms (настройка 1–60000), максимум 32 запроса
одновременно. На iframe load незавершённые запросы отклоняются как `reloaded`.
Нет фонового стрима, автоповторов и очереди кадров. В неподдерживаемой игре
будет `timeout`, а не ложное состояние «готова». Синхронный зависший getter
нельзя прервать внутри игры: provider должен быть коротким.

Коды ошибок: `disabled`, `not_ready`, `read_failed`, `invalid_state`,
`state_too_large`, `disposed`, `timeout`, `reloaded`, `invalid_response`, `busy`, `unsupported`.
Ошибка provider не выбрасывается в игровой update; host получает rejected Promise.

## Прямой debug-мост, без iframe

```ts
import { attachPlaytestDebug } from '@playbox-ai/playable-playtest/debug';
const detachDebug = attachPlaytestDebug(playtest);
// Browser/DevTools, только в явно включённом preview/playtest:
window.__PLAYBOX_PLAYTEST__.getCapabilities();
window.__PLAYBOX_PLAYTEST__.getSnapshot();
window.__PLAYBOX_PLAYTEST__.pause();
window.__PLAYBOX_PLAYTEST__.getStatus();
window.__PLAYBOX_PLAYTEST__.resume();
// При уничтожении: detachDebug(); затем отключить transport и runtime.
```

Глобал содержит только замороженный API, без App/Settings или setters состояния.
В выключенном режиме он не создаётся. Повторная регистрация без detach — ошибка.
Сохранившаяся после detach ссылка отклоняет вызовы как disposed.

Пауза использует существующий `window.playboxCapture` из Playbox hosting.
SDK не инжектит capture SDK и не зависит от платформы. Для самостоятельной игры
можно передать `attachPlaytestDebug(playtest, { pauseController })`, где controller
имеет синхронные `pause()/resume()` и getter `isPaused: boolean`. Адаптер должен
действительно останавливать цикл игры, сохранять рекламную паузу и не давать
скачка dt после resume. Без hosting/адаптера snapshot работает, pause даёт unsupported.

`getCapabilities()` → `{ snapshot: true, pause: boolean, pauseSource: 'hosting'|'game'|null }`.
`getStatus()` → `{ paused: boolean|null, ownsPause: boolean, pauseSource }`.
Повторный pause идемпотентен; resume/detach не снимают паузу, которая уже была
включена до первого вызова. У capture SDK нет токенов владения: параллельное
управление паузой из UI и агента во время опыта не поддержано.
Пауза не превращает SDK в manual/fixed-step симулятор.

Для ожидания на паузе используй часы инструмента вне страницы: hosting гейтит
page timers. Если сторонний harness подменяет Date.now, capturedAt следует его
часам; записывай реальное время получения снимка на стороне агента.

## Примеры и проверка

- [Обычная TS-игра](examples/web.ts): адаптер к фактическому состоянию игры.
- [Cocos Creator](examples/PlaytestState.ts): компонент с bind, readiness и cleanup.
- [Браузерный стенд](examples/browser/index.html): iframe, обычные кнопки и снимки.
  После build: из каталога пакета `python3 -m http.server 8090`, затем
  `http://localhost:8090/examples/browser/`.
- `npm test`: runtime + protocol checks без браузера.
- `npm run test:browser`: реальный Chromium, два origin и opaque sandbox.
  Требует Node 22+ и Chrome; при необходимости задай `CHROME_BIN`.

Агент: снимок → обычный ввод → ожидание наблюдаемого результата → новый снимок.
Сверяй скриншот и состояние. Снимок сам по себе не доказывает отсутствие
визуальных багов и не останавливает игру. Пауза доступна через `/debug` при
наличии hosting или адаптера игры; пошаговая симуляция не реализована.

Лицензия — [LICENSE](LICENSE).

---
_Source: https://npm.io/package/@playbox-ai/playable-playtest · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
