docs: agent and readme

This commit is contained in:
hypercross 2026-04-03 11:20:35 +08:00
parent 768c6ebc84
commit cf7cfcd86b
2 changed files with 129 additions and 54 deletions

View File

@ -5,7 +5,7 @@
Prefer `bash.exe`(C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps\bash.exe) over Powershell. Prefer `bash.exe`(C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps\bash.exe) over Powershell.
```bash ```bash
npm run build # Build ESM bundle + declarations to dist/ npm run build # Build ESM bundle + declarations to dist/ (via tsup)
npm run test # Run vitest in watch mode npm run test # Run vitest in watch mode
npm run test:run # Run vitest once (no watch) npm run test:run # Run vitest once (no watch)
npm run typecheck # Type-check without emitting (tsc --noEmit) npm run typecheck # Type-check without emitting (tsc --noEmit)
@ -21,18 +21,31 @@ npx vitest run tests/samples/tic-tac-toe.test.ts
npx vitest run -t "should detect horizontal win for X" 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 (entity, command, rng, async-queue)
index.ts # Single public API barrel export
tests/ # Mirrors src/ structure with *.test.ts files
```
## Code Style ## Code Style
### Imports ### Imports
- Use **double quotes** for local imports, **single quotes** for npm packages - Use **double quotes** for local imports, **single quotes** for npm packages
- Use `@/**/*` for `./src/**/*` import alias - Use `@/**/*` for `./src/**/*` import alias (configured in tsconfig.json)
- Use `@/index` for code in `samples` - Use `@/index` when importing from the public API surface in samples
- Group imports: npm packages first, then local `@/` imports, separated by blank line
### Formatting ### Formatting
- **4-space indentation** - **4-space indentation** (no tabs)
- **Semicolons** at statement ends - **Semicolons** at statement ends
- **Arrow functions** for callbacks; `function` keyword for methods needing `this` (e.g. command handlers) - **Arrow functions** for callbacks; `function` keyword for methods needing `this` (e.g. command handlers)
- No trailing whitespace - No trailing whitespace
- Trailing commas in multi-line objects/arrays
### Naming Conventions ### Naming Conventions
- **Types/Interfaces**: `PascalCase``Part`, `Region`, `Command`, `IGameContext` - **Types/Interfaces**: `PascalCase``Part`, `Region`, `Command`, `IGameContext`
@ -41,6 +54,7 @@ npx vitest run -t "should detect horizontal win for X"
- **Variables**: `camelCase` - **Variables**: `camelCase`
- **Constants**: `UPPER_SNAKE_CASE``BOARD_SIZE`, `WINNING_LINES` - **Constants**: `UPPER_SNAKE_CASE``BOARD_SIZE`, `WINNING_LINES`
- **Test files**: `*.test.ts` mirroring `src/` structure under `tests/` - **Test files**: `*.test.ts` mirroring `src/` structure under `tests/`
- **Factory functions**: prefix with `create``createEntity`, `createTestContext`
### Types ### Types
- **Strict TypeScript** is enabled — no `any` - **Strict TypeScript** is enabled — no `any`
@ -48,19 +62,22 @@ npx vitest run -t "should detect horizontal win for X"
- Use **type aliases** for object shapes (not interfaces) - Use **type aliases** for object shapes (not interfaces)
- Use **discriminated unions** for results: `{ success: true; result: T } | { success: false; error: string }` - Use **discriminated unions** for results: `{ success: true; result: T } | { success: false; error: string }`
- Use `unknown` for untyped values, narrow with type guards or `as` - Use `unknown` for untyped values, narrow with type guards or `as`
- Derive state types via `ReturnType<typeof createInitialState>`
### Error Handling ### Error Handling
- Prefer **result objects** over throwing: return `{ success, result/error }` - Prefer **result objects** over throwing: return `{ success, result/error }`
- When catching: `const error = e as Error;` then use `error.message` - When catching: `const error = e as Error;` then use `error.message`
- Use `throw new Error(...)` only for truly exceptional cases - Use `throw new Error(...)` only for truly exceptional cases
- Validation error messages are in Chinese (e.g. `"参数不足"`) - Validation error messages are in Chinese (e.g. `"参数不足"`)
- Prompt validators return `null` for valid input, `string` error message for invalid
### Testing ### Testing
- **Vitest** with globals enabled (`describe`, `it`, `expect` available without import) - **Vitest** with globals enabled (`describe`, `it`, `expect` available without import)
- Use `async/await` for async tests - Use `async/await` for async tests
- Narrow result types with `if (result.success)` before accessing `result.result` - Narrow result types with `if (result.success)` before accessing `result.result`
- Define inline helper functions in test files when needed - Define inline helper functions in test files when needed (e.g. `createTestContext()`)
- No mocking — use real implementations - No mocking — use real implementations
- For command tests: use `waitForPrompt()` + `promptEvent.tryCommit()` pattern
- Test helpers: `createTestContext()`, `createTestRegion()` - Test helpers: `createTestContext()`, `createTestRegion()`
## Architecture Notes ## Architecture Notes
@ -69,3 +86,6 @@ npx vitest run -t "should detect horizontal win for X"
- **Command system**: CLI-style parsing with schema validation via `inline-schema` - **Command system**: CLI-style parsing with schema validation via `inline-schema`
- **Prompt system**: Commands prompt for input via `PromptEvent` with `resolve`/`reject` - **Prompt system**: Commands prompt for input via `PromptEvent` with `resolve`/`reject`
- **Barrel exports**: `src/index.ts` is the single public API surface - **Barrel exports**: `src/index.ts` is the single public API surface
- **Game modules**: export `registry` and `createInitialState` for use with `createGameContextFromModule`
- **RegionEntity**: manages spatial board state with axis-based positioning and child entities
- **Mutative**: used for immutable state updates inside `Entity.produce()`

153
README.md
View File

@ -23,63 +23,113 @@ npm install boardgame-core
### Defining a Game ### Defining a Game
Each game defines its own context type by extending `IGameContext`, creates a command registry, and exports helper functions: Each game defines a command registry and exports a `createInitialState` function:
```ts ```ts
import { IGameContext, createGameCommand, createGameContext, createCommandRegistry, registerCommand } from 'boardgame-core'; import {
createGameCommandRegistry,
RegionEntity,
Entity,
} from 'boardgame-core';
// 1. Define your game-specific state // 1. Define your game-specific state
type MyGameState = { type MyGameState = {
score: number; board: RegionEntity;
round: number; score: { white: number; black: number };
currentPlayer: 'white' | 'black';
winner: 'white' | 'black' | 'draw' | null;
}; };
// 2. Extend IGameContext with your state // 2. Create initial state factory
type MyGameContext = IGameContext & { export function createInitialState(): MyGameState {
state: MyGameState; return {
}; board: new RegionEntity('board', {
id: 'board',
// 3. Define typed commands axes: [
const addScore = createGameCommand<MyGameContext, number>( { name: 'x', min: 0, max: 5 },
'add-score <amount:number>', { name: 'y', min: 0, max: 5 },
async function(cmd) { ],
this.context.state.score += cmd.params[0] as number; children: [],
return this.context.state.score; }),
} score: { white: 0, black: 0 },
); currentPlayer: 'white',
winner: null,
// 4. Create and populate a registry };
const registry = createCommandRegistry<MyGameContext>();
registerCommand(registry, addScore);
// 5. Export a context factory with initial state
export function createMyGameContext() {
return createGameContext<MyGameContext>(registry, () => ({
state: { score: 0, round: 1 },
}));
} }
// 6. Export helper functions for your game logic // 3. Create registry and register commands
export function getScore(ctx: MyGameContext) { const registration = createGameCommandRegistry<MyGameState>();
return ctx.state.score; export const registry = registration.registry;
}
registration.add('place <row:number> <col:number>', async function(cmd) {
const [row, col] = cmd.params as [number, number];
const player = this.context.value.currentPlayer;
// Mutate state via produce()
this.context.produce(state => {
state.score[player] += 1;
});
return { success: true };
});
``` ```
### Running a Game ### Running a Game
```ts ```ts
import { createMyGameContext } from './my-game'; import { createGameContext } from 'boardgame-core';
import { registry, createInitialState } from './my-game';
const game = createMyGameContext(); const game = createGameContext(registry, createInitialState);
// Run commands through the context // Run commands through the context
const result = await game.commands.run('add-score 10'); const result = await game.commands.run('place 2 3');
if (result.success) { if (result.success) {
console.log(result.result); // 10 console.log(result.result);
} }
// Access game state // Access reactive game state
console.log(game.state.score); // 10 console.log(game.state.value.score.white);
```
### Handling Player Input
Commands can prompt for player input using `this.prompt()`. Use `promptQueue.pop()` to wait for prompt events:
```ts
import { createGameContext } from 'boardgame-core';
import { registry, createInitialState } from './my-game';
const game = createGameContext(registry, createInitialState);
// Start a command that will prompt for input
const runPromise = game.commands.run('turn X 1');
// Wait for the prompt event
const promptEvent = await game.commands.promptQueue.pop();
console.log(promptEvent.schema.name); // e.g. 'play'
// Validate and submit player input
const error = promptEvent.tryCommit({
name: 'play',
params: ['X', 1, 1],
options: {},
flags: {},
});
if (error) {
console.log('Invalid move:', error);
// tryCommit can be called again with corrected input
} else {
// Input accepted, command continues
const result = await runPromise;
}
```
If the player needs to cancel instead of committing:
```ts
promptEvent.cancel('player quit');
``` ```
## Sample Games ## Sample Games
@ -94,6 +144,13 @@ See [`src/samples/tic-tac-toe.ts`](src/samples/tic-tac-toe.ts).
A more complex game with piece types (kittens/cats), supply management, the "boop" push mechanic, and graduation rules. A more complex game with piece types (kittens/cats), supply management, the "boop" push mechanic, and graduation rules.
See [`src/samples/boop/index.ts`](src/samples/boop/index.ts).
## Region System
```ts
import { RegionEntity, applyAlign, shuffle } from 'boardgame-core';
// Compact cards in a hand towards the start // Compact cards in a hand towards the start
applyAlign(handRegion); applyAlign(handRegion);
@ -101,7 +158,7 @@ applyAlign(handRegion);
shuffle(handRegion, rng); shuffle(handRegion, rng);
``` ```
### Command Parsing ## Command Parsing
```ts ```ts
import { parseCommand, parseCommandSchema, validateCommand } from 'boardgame-core'; import { parseCommand, parseCommandSchema, validateCommand } from 'boardgame-core';
@ -116,7 +173,7 @@ const result = validateCommand(cmd, schema);
// { valid: true } // { valid: true }
``` ```
### Entity Collections ## Entity Collections
```ts ```ts
import { createEntityCollection } from 'boardgame-core'; import { createEntityCollection } from 'boardgame-core';
@ -130,7 +187,7 @@ console.log(accessor.value); // reactive access
collection.remove('a'); collection.remove('a');
``` ```
### Random Number Generation ## Random Number Generation
```ts ```ts
import { createRNG } from 'boardgame-core'; import { createRNG } from 'boardgame-core';
@ -148,16 +205,16 @@ rng.setSeed(999); // reseed
| Export | Description | | Export | Description |
|---|---| |---|---|
| `IGameContext` | Base interface for the game context (parts, regions, commands, prompts) | | `IGameContext` | Base interface for the game context (state, commands) |
| `createGameContext<TContext>(registry, initialState?)` | Create a game context instance. `initialState` can be an object or factory function for custom properties | | `createGameContext(registry, initialState?)` | Create a game context instance. `initialState` can be an object or factory function |
| `createGameCommand<TContext, TResult>(schema, handler)` | Create a typed command with access to `this.context` | | `createGameCommandRegistry<TState>()` | Create a command registry with fluent `.add()` API |
### Parts ### Parts
| Export | Description | | Export | Description |
|---|---| |---|---|
| `Part` | Entity type representing a game piece | | `Part` | Type representing a game piece with sides, position, and region |
| `entity(id, data)` | Create a reactive entity | | `entity(id, data)` | Create a reactive `Entity<T>` |
| `flip(part)` | Cycle to the next side | | `flip(part)` | Cycle to the next side |
| `flipTo(part, side)` | Set to a specific side | | `flipTo(part, side)` | Set to a specific side |
| `roll(part, rng)` | Randomize side using RNG | | `roll(part, rng)` | Randomize side using RNG |
@ -166,7 +223,7 @@ rng.setSeed(999); // reseed
| Export | Description | | Export | Description |
|---|---| |---|---|
| `RegionEntity` | Entity type for spatial grouping of parts | | `RegionEntity` | Entity type for spatial grouping of parts with axis-based positioning |
| `RegionAxis` | Axis definition with min/max/align | | `RegionAxis` | Axis definition with min/max/align |
| `applyAlign(region)` | Compact parts according to axis alignment | | `applyAlign(region)` | Compact parts according to axis alignment |
| `shuffle(region, rng)` | Randomize part positions | | `shuffle(region, rng)` | Randomize part positions |
@ -191,8 +248,6 @@ rng.setSeed(999); // reseed
| `runCommand(registry, context, input)` | Parse and run a command string | | `runCommand(registry, context, input)` | Parse and run a command string |
| `runCommandParsed(registry, context, command)` | Run a pre-parsed command | | `runCommandParsed(registry, context, command)` | Run a pre-parsed command |
| `createCommandRunnerContext(registry, context)` | Create a command runner context | | `createCommandRunnerContext(registry, context)` | Create a command runner context |
| `CommandRunner` | Command runner type with schema and run function |
| `CommandRunnerContext` | Context available inside command handlers |
| `PromptEvent` | Event dispatched when a command prompts for input | | `PromptEvent` | Event dispatched when a command prompts for input |
### Utilities ### Utilities
@ -206,7 +261,7 @@ rng.setSeed(999); // reseed
## Scripts ## Scripts
```bash ```bash
npm run build # Build with tsup npm run build # Build ESM bundle + declarations to dist/
npm run test # Run tests in watch mode npm run test # Run tests in watch mode
npm run test:run # Run tests once npm run test:run # Run tests once
npm run typecheck # Type check with TypeScript npm run typecheck # Type check with TypeScript