boardgame-phaser/AGENTS.md

5.6 KiB

AGENTS.md - boardgame-phaser

Project Overview

A Phaser 3 framework for building web board games, built on top of boardgame-core (state management with Preact Signals + Mutative). Uses a pnpm monorepo with four packages:

  • packages/framework (boardgame-phaser) — Reusable library: reactive scenes, signal→Phaser bindings, input/command bridge, Preact UI components
  • packages/sample-game — Demo Tic-Tac-Toe game using the framework
  • packages/boop-game — Boop sample game
  • packages/regicide-game — Regicide game

boardgame-core Usage

For detailed boardgame-core API documentation and examples, see packages/framework/node_modules/boardgame-core/

Key concepts:

  • MutableSignal — Reactive state container with .value and .produce()
  • Command System — CLI-style parsing with schema validation and prompt support
  • Region System — Spatial management with createRegion(), applyAlign(), shuffle(), moveToRegion()
  • Part System — Game pieces with createPartsFromTable(), flip(), roll()
  • RNG — Deterministic PRNG via createRNG(seed) for reproducible game states

Quick Example

import { createGameCommandRegistry, createRegion, IGameContext } from 'boardgame-core';

type GameState = {
    board: Region;
    parts: Part<{ player: 'X' | 'O' }>[];
    currentPlayer: 'X' | 'O';
};
type Game = IGameContext<GameState>;

const registry = createGameCommandRegistry<GameState>();

registry.register('place <row:number> <col:number>', async function(game: Game, row: number, col: number) {
    await game.produceAsync(state => {
        state.parts.push({ id: `p-${row}-${col}`, regionId: 'board', position: [row, col], player: state.currentPlayer });
    });
    return true;
});

Commands

Root level

pnpm dev                    # Start sample-game dev server (Vite + HMR)
pnpm build                  # Build framework, then sample-game
pnpm build:framework        # Build framework only
pnpm preview                # Preview sample-game production build

Framework (packages/framework)

pnpm --filter boardgame-phaser build       # tsup → dist/index.js + dist/index.d.ts
pnpm --filter boardgame-phaser typecheck   # tsc --noEmit

Sample game (packages/sample-game)

pnpm --filter sample-game dev              # Vite dev server with HMR
pnpm --filter sample-game build            # tsc && vite build
pnpm --filter sample-game preview          # vite preview
pnpm --filter sample-game typecheck        # tsc --noEmit (add to scripts first)

Note: Sample game uses Tailwind CSS v4 with @tailwindcss/vite plugin and @preact/preset-vite for JSX transformation.

Dependency setup

# boardgame-core is a local dependency via symlink (link:../../../boardgame-core)
# After changes to boardgame-core, simply rebuild it:
cd ../boardgame-core && npm build
# The symlink automatically resolves to the updated dist/

Testing

Framework and regicide-game have Vitest configured:

# In packages/framework:
pnpm --filter boardgame-phaser test        # Run all tests
pnpm --filter boardgame-phaser test:watch  # Watch mode

# In packages/regicide-game:
pnpm --filter regicide-game test           # Run all tests
pnpm --filter regicide-game test:watch     # Watch mode

Code Style

Imports

  • Use ESM imports only (import/export)
  • Group imports: external libraries → workspace packages → relative imports
  • Use type imports for type-only imports: import type { Foo } from 'bar'
  • Path alias @/* maps to src/* in both packages

Formatting

  • 2-space indentation, no semicolons
  • Single quotes for strings
  • Trailing commas in multi-line objects/arrays
  • Max line length: not enforced, but keep reasonable

TypeScript

  • Strict mode enabled (see tsconfig.base.json)
  • Prefer explicit types for function return values and public class members
  • Use generics with constraints: <TState extends Record<string, unknown>>
  • Define local utility types: type DisposeFn = () => void
  • Use as any sparingly; prefer as unknown as Record<string, unknown> for type narrowing

Naming conventions

  • Classes: PascalCase (GameHostScene, PhaserGame, GameUI)
  • Interfaces: PascalCase with descriptive names (GameHostSceneOptions, PhaserGameProps)
  • Functions: camelCase (spawnEffect, bindRegion)
  • Constants: UPPER_SNAKE_CASE (CELL_SIZE, BOARD_OFFSET)
  • Type aliases: PascalCase (DisposeFn, CommandResult)
  • Factory functions: create* prefix (createRegion, createRNG)

Architecture patterns

  • GameHostScene: Abstract base class extending Phaser.Scene. Subclasses implement game-specific logic. Provides gameHost property for state access and addInterruption()/addTweenInterruption() for async flow control. Disposables are auto-cleaned on scene shutdown via this.events.on('shutdown', ...).
  • Spawner: Use spawnEffect() for data-driven object creation with signal-based configuration.
  • UI components: Preact functional components with hooks. Use className (not class) for CSS classes.
  • Phaser Bridge: Use PhaserGame, PhaserScene, and GameUI components from boardgame-phaser for React/Phaser integration.

Error handling

  • Use DisposableBag for managing multiple disposables in scenes
  • Scene effects are cleaned up via the shutdown event
  • Use as any casts only when Phaser types are incomplete (e.g., setInteractive)
  • Game commands return validation errors as string | null

JSX

  • jsxImportSource: "preact" — no React import needed
  • Use h() or JSX syntax interchangeably
  • Use className for CSS class attribute