npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@playbox-ai/playable-playtest

v0.1.0

Published

Read-only game state snapshots for AI playtesting

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/[email protected]. Для локальной разработки: 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.