# boardgame-core A state management library for board games using [Preact Signals](https://preactjs.com/guide/v10/signals/). Build turn-based board games with reactive state, entity collections, spatial regions, and a command-driven game loop. ## 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 - **Region System**: Spatial management with multi-axis positioning, alignment, and shuffling - **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 ```bash npm install boardgame-core ``` ## Writing a Game ### Defining a Game Each game defines a command registry and exports a `createInitialState` function: ```ts import { createGameCommandRegistry, Region, createRegion, } from 'boardgame-core'; // 1. Define your game-specific state type MyGameState = { board: Region; parts: Record; score: { white: number; black: number }; currentPlayer: 'white' | 'black'; winner: 'white' | 'black' | 'draw' | null; }; // 2. Create initial state factory export function createInitialState(): MyGameState { return { board: createRegion('board', [ { name: 'x', min: 0, max: 5 }, { name: 'y', min: 0, max: 5 }, ]), parts: {}, score: { white: 0, black: 0 }, currentPlayer: 'white', winner: null, }; } // 3. Create registry and register commands const registration = createGameCommandRegistry(); export const registry = registration.registry; registration.add('place ', 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 ```ts import { createGameContext } from 'boardgame-core'; import { registry, createInitialState } from './my-game'; const game = createGameContext(registry, createInitialState); // Run commands through the context const result = await game.commands.run('place 2 3'); if (result.success) { console.log(result.result); } // Access reactive game state 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('play X 1 2'); 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 ### Tic-Tac-Toe The simplest example. Shows the basic command loop, 2D board regions, and win detection. See [`src/samples/tic-tac-toe.ts`](src/samples/tic-tac-toe.ts). ### Boop 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 { applyAlign, shuffle, moveToRegion } from 'boardgame-core'; // Compact cards in a hand towards the start applyAlign(handRegion, parts); // Shuffle positions of all parts in a region shuffle(handRegion, parts, rng); // Move a part from one region to another moveToRegion(part, sourceRegion, targetRegion, [0, 0]); ``` ## 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 } ``` ## 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 ### Core | Export | Description | |---|---| | `IGameContext` | Base interface for the game context (state, commands) | | `createGameContext(registry, initialState?)` | Create a game context instance. `initialState` can be an object or factory function | | `createGameCommandRegistry()` | Create a command registry with fluent `.add()` API | ### Parts | Export | Description | |---|---| | `Part` | Type representing a game piece with sides, position, and region. `TMeta` for game-specific fields | | `PartTemplate` | Template type for creating parts (excludes `id`, requires metadata) | | `PartPool` | Pool of parts with `draw()`, `return()`, and `remaining()` methods. `parts` field is `Record` | | `createPart(template, id)` | Create a single part from a template | | `createParts(template, count, idPrefix)` | Create multiple identical parts with auto-generated IDs | | `createPartPool(template, count, idPrefix)` | Create a pool of parts for lazy loading | | `mergePartPools(...pools)` | Merge multiple part pools into one | | `findPartById(parts, id)` | Find a part by ID in a Record | | `isCellOccupied(parts, regionId, position)` | Check if a cell is occupied | | `getPartAtPosition(parts, regionId, position)` | Get the part at a specific position | | `flip(part)` | Cycle to the next side | | `flipTo(part, side)` | Set to a specific side | | `roll(part, rng)` | Randomize side using RNG | ### Regions | Export | Description | |---|---| | `Region` | Type for spatial grouping of parts with axis-based positioning | | `RegionAxis` | Axis definition with min/max/align | | `createRegion(id, axes)` | Create a new region | | `applyAlign(region, parts)` | Compact parts according to axis alignment | | `shuffle(region, parts, rng)` | Randomize part positions | | `moveToRegion(part, sourceRegion?, targetRegion, position?)` | Move a part to another region. `sourceRegion` is optional for first placement | | `moveToRegionAll(parts, sourceRegion?, targetRegion, positions?)` | Move multiple parts to another region. `parts` is `Record`. `sourceRegion` is optional for first placement | | `removeFromRegion(part, region)` | Remove a part from its region | ### Commands | Export | Description | |---|---| | `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 | | `PromptEvent` | Event dispatched when a command prompts for input | ### Utilities | Export | Description | |---|---| | `createRNG(seed?)` | Create a seeded RNG instance | | `Mulberry32RNG` | Mulberry32 PRNG class | ## Scripts ```bash npm run build # Build ESM bundle + declarations to dist/ npm run test # Run tests in watch mode npm run test:run # Run tests once npm run typecheck # Type check with TypeScript ``` ## License MIT