boardgame-phaser/docs/OnitamaGamePatterns.md

4.3 KiB

Game Architecture Patterns & Practices

Reference guide for implementing board games using boardgame-phaser framework. Explore packages/onitama-game/ and packages/sample-game/ for concrete implementations.

Architecture Overview

Games follow a layered architecture separating logic, presentation, and UI state:

packages/my-game/
├── src/
│   ├── game/          # Pure game logic (re-exported from boardgame-core)
│   ├── scenes/        # Phaser scenes (MenuScene, GameScene)
│   ├── spawners/      # Data-driven object lifecycle
│   ├── renderers/     # Renderers for game objects
│   ├── state/         # UI-only reactive state
│   ├── config.ts      # Centralized layout & style constants
│   └── ui/App.tsx     # Preact root component
Layer Responsibility
game/ State, commands, prompts, validation (pure logic)
scenes/ Phaser lifecycle, input handling, visual composition
spawners/ Reactive game object spawn/update/despawn
renderers/ Phaser visual representation of game objects
state/ UI-only reactive state (selection, hover, etc.)
ui/ Preact components bridging Phaser and DOM

Core Patterns

1. ReactiveScene / GameHostScene

Extend ReactiveScene(packages\framework\src\scenes\ReactiveScene.ts) to use reactive integration features.

  • Access game context for scene navigation
  • Use this.disposables for auto-cleanup on shutdown.

2. Spawner Pattern

Implement Spawner<TData, TObj> for data-driven objects.

  • *getData(): Yield objects that should exist.
  • getKey(): Unique identifier for diffing.
  • onSpawn(): Create Phaser objects.
  • onUpdate(): Handle data changes (animate if needed).
  • onDespawn(): Clean up with optional animation.
  • See: packages/onitama-game/src/spawners/

3. Reactive UI State

Use MutableSignal for UI-only state, separate from game state.

  • Mutate via .produce().
  • React via effect() from @preact/signals-core.
  • Clean up effects on object 'destroy' to prevent leaks.
  • See: packages/onitama-game/src/state/ui.ts

4. Custom Containers

Extend Phaser.GameObjects.Container to encapsulate visuals and state.

  • Store logical state as private signals.
  • Use effect() for reactive highlights/selections.

5. Tween Interruption

Always register state-related tweens: this.scene.addTweenInterruption(tween). Prevents visual glitches when game state changes mid-animation.

6. Scene Navigation

Use await this.sceneController.launch('SceneKey'). Register scenes in App.tsx via <PhaserScene>. Pass data via data prop.

Best Practices

  • Centralize Config: Keep CELL_SIZE, BOARD_OFFSET, colors, etc., in src/config.ts with as const. Avoid magic numbers.
  • Type Imports: Use import type { Foo } from 'bar' for type-only imports.
  • Input Handling: Use this.add.zone() for grid/cell-based input zones.
  • Cleanup: Always dispose effect() on 'destroy'. Use this.disposables.add() for scene-level resources.

Common Pitfalls

Pitfall Solution
Duplicate constants across files Export from single config.ts or shared spawner
Missing tween interruptions Always call addTweenInterruption()
Effect memory leaks this.on('destroy', () => dispose())
Hardcoded magic numbers Extract to src/config.ts
UI state staleness Clear related selections on state change

Testing

Add Vitest for UI state transitions, coordinate conversions, and spawner logic. See packages/framework/ for test setup examples.

Quick Reference

Pattern Purpose
GameHostScene Connect Phaser to game host
Spawner<T, TObj> Reactive object lifecycle
MutableSignal UI-only reactive state
effect() React to signal changes in Phaser objects
addTweenInterruption() Prevent animation race conditions
sceneController.launch() Navigate between scenes
spawnEffect() Register spawners
this.disposables.add() Auto-cleanup resources
  • AGENTS.md — Project overview, commands, and code style
  • docs/GameModule.md — GameModule implementation guide
  • packages/framework/src/boardgame-phaser source code
  • packages/onitama-game/src/ — Complete game implementation reference