一键导入
e2e
Write, debug, or update Playwright E2E tests for the game — including fast, frame-exact deterministic tests using seeded RNG and fixed-timestep stepping.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Write, debug, or update Playwright E2E tests for the game — including fast, frame-exact deterministic tests using seeded RNG and fixed-timestep stepping.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Drive a headless Chromium browser to take screenshots of the running dev server so Claude can VISUALLY verify its own UI/canvas work via vision-inspection. Use whenever the user asks to "visually check", "take a screenshot", "see how it looks", "verify visually", or after touching code in src/ui/, src/rendering/, src/game/, or anything that produces visible output. This is for ad-hoc visual inspection, NOT for writing Playwright test specs (use the e2e skill for that).
Safely refactor a module with full test coverage verification
Run full verification suite (lint, test, build) and report results
基于 SOC 职业分类
| name | e2e |
| description | Write, debug, or update Playwright E2E tests for the game — including fast, frame-exact deterministic tests using seeded RNG and fixed-timestep stepping. |
| user-invocable | true |
| allowed-tools | Bash, Read, Edit, Write, Grep, Glob |
| argument-hint | [test-name or 'debug' or 'update-baselines'] |
Work with Playwright E2E tests for this canvas game. The argument ($ARGUMENTS) determines the action:
debug: Diagnose why E2E tests are failingupdate-baselines: Regenerate visual screenshot baselinespackage.json pins packageManager (pnpm@11.x) and engines.node >=24.node -v is wrong in a shell: source "$NVM_DIR/nvm.sh" && nvm use 24.npx playwright install chrome-for-testing chromium-headless-shell.pnpm run e2e (headless, CI mode) — see Commands below.e2e/
playwright.config.ts # Config: Chromium, 1920x1080, webServer on :3000
fixtures/game.fixture.ts # GameHelpers: waitForState, stepFrames, seedRandom, etc.
helpers/game-bridge.ts # TypeScript types for window.__GAME__ bridge API
tests/*.spec.ts # Test files
tests/*.spec.ts-snapshots/ # Visual baselines (platform-specific PNGs)
src/testing/e2eTestBridge.ts # Bridge installed in dev mode — exposes game state on window.__GAME__
src/utils/gameplayRng.ts # Seeded gameplay RNG — the seam that makes runs reproducible
Use seedRandom() + startDeterministic() + stepFrames(). The simulation
advances by a fixed number of fixed-delta frames, decoupled from the wall
clock, so 30 simulated seconds run in milliseconds and produce the same
result every run. Assert on exact state — no toBeGreaterThan, no generous
timeouts. Reference: e2e/tests/deterministic-gameplay.spec.ts.
import { test, expect } from '../fixtures/game.fixture';
test('seeded battle is reproducible', async ({ page, game }) => {
async function run(seed: number) {
await page.goto('/');
await game.waitForGameReady();
await game.seedRandom(seed); // seed BEFORE startDeterministic
await game.startDeterministic(); // starts game + freezes the wall-clock ticker
await page.evaluate(() => { // disable AI for byte-exact runs (see caveat)
if (window.__GAME__.isAIEnabled()) window.__GAME__.toggleAI();
});
await game.waitForState('PLAYING');
await page.evaluate(() => window.__GAME__.setResources(99999));
await page.evaluate(() => window.__GAME__.placeTurret(1, 960, 340));
await game.stepFrames(1800); // advance exactly 1800 frames (30s @ 60fps)
return game.getSnapshot();
}
expect(await run(424242)).toEqual(await run(424242));
});
Rules for deterministic tests:
seedRandom(seed) before startDeterministic() — wave 1 spawns during start.startDeterministic(), not startGame() — it freezes the ticker so no
variable number of real frames tick between async test steps.stepFrames(n). Setup calls (setResources,
placeTurret, freezeStarfield) are safe between steps — the sim is frozen.toggleAI()). Core simulation (spawns, combat, kills, health,
waves) is fully deterministic with AI off. Cover the AI path separately with
loose-bound assertions.The classic approach — startGame(), then wait for real time. Needs generous
timeouts. Use only when deterministic stepping doesn't fit (e.g. testing the
real-time loop, input handling, or menu transitions).
test('wave 1 spawns enemies', async ({ page, game }) => {
await page.goto('/');
await game.waitForGameReady();
await game.startGame();
await game.waitForState('PLAYING');
await game.waitForEnemies(20000); // SwiftShader on CI is ~5x slower
expect((await game.getSnapshot()).activeEnemies).toBeGreaterThan(0);
});
e2e/fixtures/game.fixture.ts for available helpers.e2e/helpers/game-bridge.ts for the full bridge API.e2e/tests/deterministic-gameplay.spec.ts (deterministic) or
e2e/tests/gameplay.spec.ts (wall-clock) for patterns.import { test, expect } from '../fixtures/game.fixture'.game.freezeStarfield() before any screenshot.// 20s enemies + 45s kills = 65s < 90s test timeout.Source of truth: src/testing/e2eTestBridge.ts. Key methods:
| Method | Purpose |
|---|---|
isReady() | True once the game is initialized |
startGame() | Start a game (wall-clock ticker runs) |
startDeterministic() | Start a game with the ticker frozen — advance only via stepFrames() |
seedRandom(seed) | Seed the gameplay RNG (mulberry32) — makes spawns/combat reproducible |
stepFrames(n, deltaMs?) | Advance exactly n fixed-delta frames (default 1/60s) |
pause() / resume() / restart() | Lifecycle |
getSnapshot() | { state, wave, waveState, activeEnemies, resources, score, kmHealth, kmMaxHealth } |
getEvents() / getEventsByType(t) / clearEvents() | Captured event log |
getEntityCounts() | { turrets, enemies } |
setResources(n) / setKMHealth(n) / placeTurret(type,x,y) | State manipulation |
toggleGodMode() / toggleAI() / isAIEnabled() | Cheats / AI control |
freezeStarfield() | Freeze the animated starfield before screenshots |
Keep all three layers in sync:
src/testing/e2eTestBridge.ts — the window.__GAME__ objecte2e/helpers/game-bridge.ts — the GameBridge interfacee2e/fixtures/game.fixture.ts — the GameHelpers interface + implementationsrc/utils/gameplayRng.ts — a seeded mulberry32 stream. Unseeded it falls
back to Math.random(), so production behaviour is unchanged.randomFloat() from this module.
Rendering/particle randomness deliberately stays on Math.random() so visual
effects can never desync the simulation.GameLoopManager.step() runs the update phases with a fixed delta;
Game.stepFrames() stops the wall-clock ticker and steps.randomFloat from ../utils —
not Math.random() — or you reintroduce nondeterminism.pnpm run e2e # Headless (CI mode) — default
pnpm run e2e:headed # Watch in a visible browser
pnpm run e2e:ui # Interactive Playwright UI
pnpm run e2e:update # Regenerate visual baselines
# Single spec:
npx playwright test --config e2e/playwright.config.ts deterministic-gameplay
pnpm run e2e:headed to watch in browser, or npx playwright test ... --workers=1.e2e/test-results/ for failure screenshots + traces.seedRandom() before
startDeterministic()? Is the AI disabled? Did new code call
Math.random() instead of randomFloat() in a gameplay path?pnpm run e2e:update).*-chromium-darwin.png; CI is
*-chromium-linux.png. Visual tests skip on CI.| Turret | Type | Range | Damage | Notes |
|---|---|---|---|---|
| Phaser | 0 | 200px | 10 | Short range |
| Torpedo | 1 | 350px | 60 | Best for tests |
| Beam | 2 | 250px | 30 | Continuous |
Game states: MENU → PLAYING → PAUSED / GAME_OVER.
KM center is world (960, 540) on a 1920×1080 canvas.