Getting started
Requirements
- Node.js 22 or later.
- A game that loads through an
index.htmlwith<script type="module" src="…">, as Vite projects do. Keiko uses your project's Vite config if it has one, so aliases and plugins work.
Install
npm install -D keiko
npx keiko initkeiko init writes a first test for the engine your project uses (it boots, and plays 5 seconds without an error) and prints a window.__KEIKO__ hook to fill in later. It never overwrites a file. Phaser 3 and PixiJS 8 games can start without the hook: keiko reads the scene and the buttons on screen from the engine (no hook needed).
Using debuff? New projects already have keiko and example tests. Run them with npx debuff test and skip to Playing the game.
Expose your game's state
Tests assert on a snapshot of your game. Add one in development builds, as JSON-friendly data:
// src/main.ts
if (import.meta.env.DEV) {
window.__KEIKO__ = {
state: () => ({ player: { x: player.x, y: player.y, vx: player.vx }, score, lives }),
scene: () => currentScene, // optional
};
}import.meta.env.DEV is false in production builds, so bundlers drop this code. Put in what you'll want to check: positions, speeds, counts, flags. For TypeScript, add import type {} from "keiko"; to that file to type window.__KEIKO__.
You don't need anything else to play the game: Keiko steps your requestAnimationFrame loop and sends real keyboard and pointer events. See Exposing your game for params, commands and engines with their own fixed step.
Write a test
Create tests/game.test.ts:
import { expect, play, test } from "keiko";
interface State {
player: { x: number; y: number; vx: number };
score: number;
lives: number;
}
test("the player walks right while ArrowRight is held", async () => {
const game = await play<State>();
const start = game.state.player.x;
game.hold("ArrowRight", "1s");
expect(game.state.player.x).toBeAbove(start + 50);
});If you use TypeScript, add "tests" to include in tsconfig.json.
Run it
npx keikokeiko 稽古 · practice for web games
pass tests/game.test.ts › the player walks right while ArrowRight is held 41 ms
✔ 1 passed in 612 msKeiko finds *.test.ts (and .js, .mts, .mjs) files in tests/ and src/. The first test takes longer while Vite warms up; after that each game boots in tens of milliseconds.
What play() does
- Opens a fresh headless page and loads your
index.html's markup. - Runs the page's module scripts (your game) through Vite, on a virtual clock with a seeded
Math.random(). - Waits for the game's first network requests (assets, data) to finish, and for
readyif you passed one. - Plays the first frame, as a browser paints before a player can act.
Each play() is a brand-new game. Nothing carries over between tests.
Next
- Playing the game: time, input, params and commands.
- Asking game questions:
until,duringandtrace. - Engine recipes: Phaser, PixiJS, KAPLAY, Excalibur, three.js.