npm.io
0.1.0 • Published yesterday

@playbox-ai/playable-playtest

Licence
UNLICENSED
Version
0.1.0
Deps
0
Size
40 kB
Vulns
0
Weekly
0

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 и внешней оболочке выполняется отдельно.

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.

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

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

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-игра: адаптер к фактическому состоянию игры.
  • Cocos Creator: компонент с bind, readiness и cleanup.
  • Браузерный стенд: 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.