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 two 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

boardgame-core Usage

For detailed boardgame-core API documentation and examples, see boardgame-core Guide.

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 createPart(), createPartPool(), flip(), roll()
  • RNG — Deterministic PRNG via createRNG(seed) for reproducible game states

Quick Example

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

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

const registration = createGameCommandRegistry<GameState>();
export const registry = registration.registry;

registration.add('place <row:number> <col:number>', async function(cmd) {
    const [row, col] = cmd.params as [number, number];
    this.context.produce(state => {
        state.parts.push({ id: `p-${row}-${col}`, regionId: 'board', position: [row, col], player: state.currentPlayer });
    });
    return { success: 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 && pnpm build
# The symlink automatically resolves to the updated dist/

Testing

No test framework is configured yet. When adding tests, use Vitest (consistent with boardgame-core):

# In packages/framework:
pnpm add -D vitest
# Run all tests:
pnpm vitest run
# Run a single test file:
pnpm vitest run src/bindings/index.test.ts
# Run tests matching a pattern:
pnpm vitest run -t "bindRegion"
# Watch mode:
pnpm vitest

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 (ReactiveScene, InputMapper, PromptHandler)
  • Interfaces: PascalCase with descriptive names (ReactiveSceneOptions, BindRegionOptions)
  • Functions: camelCase (bindSignal, createInputMapper, createPromptHandler)
  • Constants: UPPER_SNAKE_CASE (CELL_SIZE, BOARD_OFFSET)
  • Type aliases: PascalCase (DisposeFn, CommandResult)
  • Factory functions: create* prefix (createInputMapper, createPromptHandler)

Architecture patterns

  • ReactiveScene: Abstract base class extending Phaser.Scene. Subclasses implement onStateReady() and setupBindings(). Effects are auto-cleaned on scene shutdown via this.events.on('shutdown', ...).
  • Bindings: Use effect() from @preact/signals-core to reactively sync signal state to Phaser objects. Always return cleanup functions.
  • Input/Command bridge: Maps Phaser pointer events to boardgame-core command strings. Uses create* factory pattern.
  • UI components: Preact functional components with hooks. Use className (not class) for CSS classes.

Error handling

  • Command results use { success: true, result } | { success: false, error } pattern
  • Scene effects are cleaned up via the shutdown event, not by overriding shutdown()
  • Use as any casts only when Phaser types are incomplete (e.g., setInteractive)
  • Prompt handlers return string | null for validation errors

JSX

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