docs: agent and readme
This commit is contained in:
parent
768c6ebc84
commit
cf7cfcd86b
30
AGENTS.md
30
AGENTS.md
|
|
@ -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
153
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue