mcpField
P
phaser-playtest

phaser-playtest

github.com/yakoub-aiTesting & quality29127 installsUpdated 9w ago
Repository

About phaser-playtest

phaser-playtest is a runtime verification skill for Phaser 4 games. Its Playwright-based harness checks browser loading, scenes, rendering, assets, frame rate, and errors, and supports scripted scenarios for regression testing.

Key features

  1. 01Headless Chromium runtime checks
  2. 02Scene, canvas, and asset verification
  3. 03FPS sampling and error detection
  4. 04Scripted input and state assertions
  5. 05Development and build test modes
  6. 06JSON reports and screenshots

Use cases

  1. 01Verifying game changes beyond TypeScript checks
  2. 02Testing gameplay acceptance criteria
  3. 03Reproducing scripted play sessions
  4. 04Running game checks in CI

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/yakoub-ai/phaser4-gamedev --skill phaser-playtest

After installing, type/phaser-playtestin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

examples/
scenario.example.mjs
references/
instrumenting-games.md
playtest-harness.md
scripts/
playtest.mjs

Details

From the project README

Phaser 4 Playtesting (Runtime Verification)

npx tsc --noEmit proves the code compiles. It says nothing about whether the game runs. Every failure below type-checks perfectly and still ships a broken game:

FailureWhat the player seesWhat TypeScript says
Asset path typoInvisible spritesNothing — it's a string
Scene missing from scene: []Black screenNothing
create() throws after first lineHalf-built sceneNothing
Depth/alpha/camera-scroll mistakeBlank canvasNothing
Physics body never addedPlayer falls through floorNothing
Uncapped emitter12 fpsNothing

Runtime verification is not optional. Run the harness after any change that touches scene lifecycle, asset loading, physics, or rendering — before telling the user it works.

Run It

node "${CLAUDE_PLUGIN_ROOT}/skills/phaser-playtest/scripts/playtest.mjs" --project .

The harness starts the dev server itself, drives headless Chromium, and shuts the server down again. No config, no test files required.

Requires Playwright in the project (once):

npm install -D playwright && npx playwright install chromium
What it checks
  1. Page loads — HTTP status of the document
  2. Canvas created — a sized <canvas> exists (fails ⇒ Phaser never booted)
  3. Phaser instance reachable — finds the Phaser.Game on window
  4. Game booted — isBooted === true
  5. Active scenes — which scenes run, and how many display objects each holds
  6. Scenes render content — warns on a scene with an empty display list
  7. Frame rate — median and 5th-percentile FPS sampled over real frames
  8. Canvas renders content — pixel analysis; catches the black screen
  9. No uncaught exceptions — pageerror with stack
  10. No console errors
  11. All assets load — including files the dev server masks as 200 text/html

Exit code 0 = pass, 1 = check failures, 2 = harness error. Writes .playtest/report.json plus PNG screenshots.

Options
FlagPurpose
--project DIRProject root (default: cwd)
--url URLTest an already-running server instead of starting one
--mode dev|buildbuild runs npm run build + preview — catches base-path and bundling bugs
--scenario FILEDrive a scripted play session (see below)
--device iphone|androidMobile viewport, DPR, touch, and UA
--settle MSWait before probing (default 3000; raise for slow preloads)
--fail-on-warnTreat console warnings as failures (CI)
--jsonEmit only JSON — use when parsing results
--headedShow the browser

Make the Game Testable (one line)

The deep checks need the game instance. Bundled games keep it in module scope, so expose it in dev builds only:

const game = new Phaser.Game(config);
if (import.meta.env.DEV) (window as any).__PHASER_GAME__ = game;
export default game;

Without it the harness still catches black screens, exceptions, and 404s, but skips scene, FPS, and state assertions — and says so. Add the line; it costs nothing in production. See references/instrumenting-games.md for deterministic-testing hooks (seeded RNG, time control, state injection).

Scenario Scripts

A scenario drives input and asserts on live game state. game is bound to the running instance inside every expression.

// playtest/combat.mjs
export default [
  { name: 'reaches gameplay', action: 'expect',
    expect: { expression: `game.scene.isActive('GameScene')`, equals: true } },

  { name: 'walk right', action: 'key', key: 'ArrowRight', duration: 600 },

  { name: 'player advanced', action: 'expect',
    expect: { expression: `game.scene.getScene('GameScene').player.x > 400`, equals: true } },

  { name: 'attack lands', action: 'press', key: 'Space',
    expect: { expression: `game.registry.get('enemyHp')`, atMost: 90 } },

  { name: 'after-combat', action: 'screenshot' },

  { name: 'holds frame rate', action: 'expect',
    expect: { expression: `game.loop.actualFps`, atLeast: 55 } },
];

Actions: wait (ms), key (key, duration), press (key), click (x, y canvas-relative), screenshot (name), expect. Assertions: equals (deep), atLeast, atMost, or bare expression for truthy.

Full reference: references/playtest-harness.md. Worked example: examples/scenario.example.mjs.

Turning a Plan into Tests

Acceptance criteria from /phaser-gdd or the phaser-architect agent translate directly into scenario steps. Write the scenario when the feature is specified, not after it breaks:

Acceptance criterionScenario assertion
"Player jumps 3 tiles high"game.scene.getScene('GameScene').player.y, atMost: spawnY - 96
"Enemy dies in 3 hits"press attack ×3, then enemy.active, equals: false
"Score persists across scenes"start GameOver, then game.registry.get('score'), atLeast: 10
"Runs at 60fps with 50 enemies"spawn via eval, then game.loop.actualFps, atLeast: 55

Reading a Failure

Harness outputRoot cause to check first
canvas created failsScene threw in constructor/init, or the bundle 500s. Read console errors.
active scenes: no scene is runningScene missing from scene: [], or create() threw before completing
canvas renders content blank, scenes activeObjects off-camera, alpha: 0, wrong depth, or camera not following
scenes render content warns emptycreate() returned early — usually an exception swallowed by a try
all assets load fails with text/htmlPath typo, or the asset is in src/ instead of public/
frame rate lowUncapped particles, no object pooling, per-frame allocation. Use /phaser-analyze.
Phaser game instance found warnsAdd the __PHASER_GAME__ line above

CI

- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx tsc --noEmit
- run: node scripts/playtest.mjs --project . --mode build --fail-on-warn

Copy playtest.mjs into the project's own scripts/ so CI does not depend on the plugin being installed.

Discipline

  • Run the harness before reporting a feature complete — not after the user reports a bug.
  • A passing tsc plus a failing playtest means the work is not done.
  • When it fails, read .playtest/report.json and the screenshots before editing code. Investigation-first applies here exactly as in phaser-debugger.
  • Keep a scenario per major feature under playtest/; they are regression tests.

When the failure came from a player

If you are here because someone reported a bug rather than because you just changed code, use skills/phaser-feedback/ instead of writing a scenario straight away. It covers triaging the report first (a "too hard" complaint is not a defect and has no repro), and maps each claim shape to the right construct — --repeat for "sometimes", sample for "gets stuck", waitFor for "takes too long", --heap for "slows down over time".

Comments

Markdown supported