boardgame-phaser/AGENTS.md

136 lines
5.6 KiB
Markdown

# 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
```ts
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
```bash
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`)
```bash
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`)
```bash
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
```bash
# 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:
```bash
# 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