From 768c6ebc844dca0d9b2e15547c652fd6037be40c Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 3 Apr 2026 10:07:12 +0800 Subject: [PATCH] refactor: update readme --- README.md | 243 +++++++++++++++++++++++------------------------------- 1 file changed, 105 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index eb65ff1..0d8a696 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Build turn-based board games with reactive state, entity collections, spatial re ## Features - **Reactive State Management**: Fine-grained reactivity powered by [@preact/signals-core](https://preactjs.com/guide/v10/signals/) +- **Type Safe**: Full TypeScript support with strict mode and generic context extension - **Entity Collections**: Signal-backed collections for managing game pieces (cards, dice, tokens, meeples, etc.) - **Region System**: Spatial management with multi-axis positioning, alignment, and shuffling -- **Command Parsing**: CLI-style command parsing with schema validation and type coercion -- **Rule Engine**: Generator-based rule system with reactive context management +- **Command System**: CLI-style command parsing with schema validation, type coercion, and prompt support - **Deterministic RNG**: Seeded pseudo-random number generator (Mulberry32) for reproducible game states ## Installation @@ -21,156 +21,65 @@ npm install boardgame-core ## Writing a Game -The core pattern for writing a game is: +### Defining a Game -1. Define your game state type -2. Create a command registry with `createGameCommandRegistry()` -3. Register commands for `setup`, `turn`, and any actions -4. Run `setup` to start the game loop - -### Step 1: Define Your State - -Every game needs a state type. Use `RegionEntity` for boards/zones and plain fields for everything else. +Each game defines its own context type by extending `IGameContext`, creates a command registry, and exports helper functions: ```ts -import { RegionEntity, Entity } from 'boardgame-core'; +import { IGameContext, createGameCommand, createGameContext, createCommandRegistry, registerCommand } from 'boardgame-core'; -type Player = 'X' | 'O'; - -type GameState = { - board: RegionEntity; - parts: Entity[]; - currentPlayer: Player; - winner: Player | 'draw' | null; - turn: number; +// 1. Define your game-specific state +type MyGameState = { + score: number; + round: number; }; -``` -### Step 2: Create the Command Registry +// 2. Extend IGameContext with your state +type MyGameContext = IGameContext & { + state: MyGameState; +}; -`createGameCommandRegistry` ties your state type to the command system. Commands are registered with a schema string and an async handler. - -```ts -import { createGameCommandRegistry } from 'boardgame-core'; - -const registration = createGameCommandRegistry(); -export const registry = registration.registry; -``` - -### Step 3: Register the `setup` Command - -The `setup` command is the main game loop. It runs turns until a winner is determined. - -```ts -registration.add('setup', async function() { - const { context } = this; - while (true) { - const currentPlayer = context.value.currentPlayer; - const turnOutput = await this.run<{ winner: WinnerType }>(`turn ${currentPlayer}`); - if (!turnOutput.success) throw new Error(turnOutput.error); - - context.produce(state => { - state.winner = turnOutput.result.winner; - if (!state.winner) { - state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X'; - state.turn++; - } - }); - if (context.value.winner) break; +// 3. Define typed commands +const addScore = createGameCommand( + 'add-score ', + async function(cmd) { + this.context.state.score += cmd.params[0] as number; + return this.context.state.score; } - return context.value; -}); -``` +); -### Step 4: Register the `turn` Command +// 4. Create and populate a registry +const registry = createCommandRegistry(); +registerCommand(registry, addScore); -The `turn` command handles a single player's turn. Use `this.prompt()` to request validated input from the player. +// 5. Export a context factory with initial state +export function createMyGameContext() { + return createGameContext(registry, () => ({ + state: { score: 0, round: 1 }, + })); +} -```ts -registration.add('turn ', async function(cmd) { - const [turnPlayer] = cmd.params as [Player]; - - const playCmd = await this.prompt( - 'play ', - (command) => { - const [player, row, col] = command.params as [Player, number, number]; - if (player !== turnPlayer) return `Wrong player.`; - if (isCellOccupied(this.context, row, col)) return `Cell occupied.`; - return null; // null = valid - } - ); - const [, row, col] = playCmd.params as [Player, number, number]; - - placePiece(this.context, row, col, turnPlayer); - - const winner = checkWinner(this.context); - return { winner }; -}); -``` - -### Managing Part Movement - -Move parts between regions with `moveToRegion`, `moveToRegionAll`, and `removeFromRegion`. - -```ts -import { moveToRegion, moveToRegionAll, removeFromRegion } from 'boardgame-core'; - -// Move a single piece to a new region with a new position -moveToRegion(card, handRegion, [0]); - -// Move a single piece, keeping its current position -moveToRegion(card, handRegion); - -// Move multiple pieces at once with new positions -moveToRegionAll([card1, card2, card3], discardPile, [[0], [1], [2]]); - -// Remove a piece from its region (without adding to another) -removeFromRegion(card); -``` - -### Step 5: Manage Parts on the Board - -Parts are game pieces placed inside regions. Use `entity()` to create reactive entities and `produce()` to mutate state. - -```ts -import { entity } from 'boardgame-core'; - -function placePiece(host: Entity, row: number, col: number, player: Player) { - const board = host.value.board; - const piece = { - id: `piece-${player}-${host.value.parts.length + 1}`, - region: board, - position: [row, col], - player, - }; - host.produce(state => { - const e = entity(piece.id, piece); - state.parts.push(e); - board.produce(draft => { - draft.children.push(e); - }); - }); +// 6. Export helper functions for your game logic +export function getScore(ctx: MyGameContext) { + return ctx.state.score; } ``` -### Step 6: Run the Game +### Running a Game ```ts -import { createGameContextFromModule } from 'boardgame-core'; -import * as yourGame from './your-game'; +import { createMyGameContext } from './my-game'; -const game = createGameContextFromModule(yourGame); -game.commands.run('setup'); -``` +const game = createMyGameContext(); -Or use `createGameContext` directly: +// Run commands through the context +const result = await game.commands.run('add-score 10'); +if (result.success) { + console.log(result.result); // 10 +} -```ts -import { createGameContext } from 'boardgame-core'; -import { registry, createInitialState } from './your-game'; - -const game = createGameContext(registry, createInitialState); -game.commands.run('setup'); +// Access game state +console.log(game.state.score); // 10 ``` ## Sample Games @@ -185,7 +94,53 @@ 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. -See [`src/samples/boop/index.ts`](src/samples/boop/index.ts) and [Boop rules](src/samples/boop/rules.md). +// Compact cards in a hand towards the start +applyAlign(handRegion); + +// Shuffle positions of all parts in a region +shuffle(handRegion, rng); +``` + +### Command Parsing + +```ts +import { parseCommand, parseCommandSchema, validateCommand } from 'boardgame-core'; + +// Parse a command string +const cmd = parseCommand('move card1 hand --force -x 10'); +// { name: 'move', params: ['card1', 'hand'], flags: { force: true }, options: { x: '10' } } + +// Define and validate against a schema +const schema = parseCommandSchema('move [--force] [-x: number]'); +const result = validateCommand(cmd, schema); +// { valid: true } +``` + +### Entity Collections + +```ts +import { createEntityCollection } from 'boardgame-core'; + +const collection = createEntityCollection(); +collection.add({ id: 'a', name: 'Item A' }, { id: 'b', name: 'Item B' }); + +const accessor = collection.get('a'); +console.log(accessor.value); // reactive access + +collection.remove('a'); +``` + +### Random Number Generation + +```ts +import { createRNG } from 'boardgame-core'; + +const rng = createRNG(12345); +rng.nextInt(6); // 0-5 +rng.next(); // [0, 1) +rng.next(100); // [0, 100) +rng.setSeed(999); // reseed +``` ## API Reference @@ -193,10 +148,9 @@ See [`src/samples/boop/index.ts`](src/samples/boop/index.ts) and [Boop rules](sr | Export | Description | |---|---| -| `createGameContext(root?)` | Create a new game context instance | -| `createGameCommandRegistry()` | Create a typed command registry for your game state | -| `GameContext` | The game context model class | -| `invokeRuleContext(pushContext, type, rule)` | Execute a rule with context management | +| `IGameContext` | Base interface for the game context (parts, regions, commands, prompts) | +| `createGameContext(registry, initialState?)` | Create a game context instance. `initialState` can be an object or factory function for custom properties | +| `createGameCommand(schema, handler)` | Create a typed command with access to `this.context` | ### Parts @@ -227,6 +181,19 @@ See [`src/samples/boop/index.ts`](src/samples/boop/index.ts) and [Boop rules](sr | `parseCommand(input)` | Parse a command string into a `Command` object | | `parseCommandSchema(schema)` | Parse a schema string into a `CommandSchema` | | `validateCommand(cmd, schema)` | Validate a command against a schema | +| `parseCommandWithSchema(cmd, schema)` | Parse and validate in one step | +| `applyCommandSchema(cmd, schema)` | Apply schema validation and return validated command | +| `createCommandRegistry()` | Create a new command registry | +| `registerCommand(registry, runner)` | Register a command runner | +| `unregisterCommand(registry, name)` | Remove a command from the registry | +| `hasCommand(registry, name)` | Check if a command exists | +| `getCommand(registry, name)` | Get a command runner by name | +| `runCommand(registry, context, input)` | Parse and run a command string | +| `runCommandParsed(registry, context, command)` | Run a pre-parsed command | +| `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 | ### Utilities