# AGENTS.md - boardgame-core ## Commands Prefer `bash.exe`(C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps\bash.exe) over Powershell. ```bash 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 ```bash npx vitest run tests/samples/tic-tac-toe.test.ts ``` ### Running a single test by name ```bash 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**: `PascalCase` — `Part`, `Region`, `Command`, `IGameContext` - **Classes**: `PascalCase` — `MutableSignal`, `AsyncQueue`, `Mulberry32RNG` - **Functions**: `camelCase`, verb-first — `createGameContext`, `parseCommand`, `isValidMove` - **Variables**: `camelCase` - **Constants**: `UPPER_SNAKE_CASE` — `BOARD_SIZE`, `WINNING_LINES` - **Test files**: `*.test.ts` mirroring `src/` structure under `tests/` - **Factory functions**: prefix with `create` or `mutable` — `createGameContext`, `mutableSignal` ### Types - **Strict TypeScript** is enabled — no `any` - Use **generics** heavily: `MutableSignal`, `CommandRunner` - 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` ### 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` 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 - **Barrel exports**: `src/index.ts` is the single public API surface - **Game modules**: export `registry` and `createInitialState` for use with `createGameContextFromModule` - **Region system**: plain `Region` type with `createRegion()` factory; `parts` are stored as `Record` keyed by ID, with `partMap` in regions mapping position keys to part IDs - **Part collections**: Game state uses `Record>` (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()`