boardgame-core/AGENTS.md

4.7 KiB

AGENTS.md - boardgame-core

Commands

Prefer bash.exe(C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps\bash.exe) over Powershell.

npm run build        # Build ESM bundle + declarations to dist/ (via tsup)
npm run test         # Run vitest in watch mode
npm run test:run     # Run vitest once (no watch)
npm run typecheck    # Type-check without emitting (tsc --noEmit)

Running a single test file

npx vitest run tests/samples/tic-tac-toe.test.ts

Running a single test by name

npx vitest run -t "should detect horizontal win for X"

Project Structure

src/
  core/        # Core game primitives (game, part, region)
  samples/     # Example games (tic-tac-toe, boop)
  utils/       # Shared utilities (mutable-signal, command, rng, async-queue)
  index.ts     # Single public API barrel export
tests/         # Mirrors src/ structure with *.test.ts files

Code Style

Imports

  • Use double quotes for local imports, single quotes for npm packages
  • Use @/**/* for ./src/**/* import alias (configured in tsconfig.json)
  • Use @/index when importing from the public API surface in samples
  • Group imports: npm packages first, then local @/ imports, separated by blank line

Formatting

  • 4-space indentation (no tabs)
  • Semicolons at statement ends
  • Arrow functions for callbacks; function keyword for methods needing this (e.g. command handlers)
  • No trailing whitespace
  • Trailing commas in multi-line objects/arrays

Naming Conventions

  • Types/Interfaces: PascalCasePart, Region, Command, IGameContext, GameHost
  • Classes: PascalCaseMutableSignal, AsyncQueue, Mulberry32RNG, GameHost
  • Functions: camelCase, verb-first — createGameContext, createGameHost, parseCommand, isValidMove
  • Variables: camelCase
  • Constants: UPPER_SNAKE_CASEBOARD_SIZE, WINNING_LINES
  • Test files: *.test.ts mirroring src/ structure under tests/
  • Factory functions: prefix with create or mutablecreateGameContext, createGameHost, mutableSignal

Types

  • Strict TypeScript is enabled — no any
  • Use generics heavily: MutableSignal<T>, CommandRunner<TContext, TResult>
  • Use type aliases for object shapes (not interfaces)
  • Use discriminated unions for results: { success: true; result: T } | { success: false; error: string }
  • Use unknown for untyped values, narrow with type guards or as
  • Derive state types via ReturnType<typeof createInitialState>

Error Handling

  • Prefer result objects over throwing: return { success, result/error }
  • When catching: const error = e as Error; then use error.message
  • Use throw new Error(...) only for truly exceptional cases
  • Validation error messages are in Chinese (e.g. "参数不足")
  • Prompt validators return null for valid input, string error message for invalid

Testing

  • Vitest with globals enabled (describe, it, expect available without import)
  • Use async/await for async tests
  • Narrow result types with if (result.success) before accessing result.result
  • Define inline helper functions in test files when needed (e.g. createTestContext())
  • No mocking — use real implementations
  • For command tests: use waitForPrompt() + promptEvent.tryCommit() pattern
  • Test helpers: createTestContext(), createTestRegion()

Architecture Notes

  • Reactivity: MutableSignal<T> extends Preact Signal — access state via .value, mutate via .produce(draft => ...)
  • Command system: CLI-style parsing with schema validation via inline-schema
  • Prompt system: Commands prompt for input via PromptEvent with resolve/reject; tryCommit accepts Command | string and validates against schema before custom validator; promptEnd event fires when prompt completes (success or cancel)
  • GameHost: Lifecycle manager for game sessions — provides setup(), onInput(), dispose(), reactive state/status/activePromptSchema signals
  • Barrel exports: src/index.ts is the single public API surface
  • Game modules: export registry and createInitialState for use with createGameContextFromModule or createGameHost
  • Region system: plain Region type with createRegion() factory; parts are stored as Record<string, Part> keyed by ID, with partMap in regions mapping position keys to part IDs
  • Part collections: Game state uses Record<string, Part<TMeta>> (not arrays) for O(1) lookup by ID. Use Object.values(parts) when iteration is needed, Object.keys(parts) for count/IDs
  • Mutative: used for immutable state updates inside MutableSignal.produce()