board game state management
Go to file
hypercross 768c6ebc84 refactor: update readme 2026-04-03 10:07:40 +08:00
src feat: add new functions 2026-04-02 21:58:11 +08:00
tests feat: add new functions 2026-04-02 21:58:11 +08:00
.gitignore Initial commit: boardgame-core with build fixes 2026-03-31 18:01:57 +08:00
AGENTS.md refactor: add src alias to @/ 2026-04-02 16:03:44 +08:00
README.md refactor: update readme 2026-04-03 10:07:40 +08:00
package-lock.json chore: switch to npm link based dependencies 2026-04-02 21:07:43 +08:00
package.json chore: switch to npm link based dependencies 2026-04-02 21:07:43 +08:00
tsconfig.json refactor: add src alias to @/ 2026-04-02 16:03:44 +08:00
tsup.config.ts refactor: add src alias to @/ 2026-04-02 16:03:44 +08:00
vitest.config.ts refactor: add src alias to @/ 2026-04-02 16:03:44 +08:00

README.md

boardgame-core

A state management library for board games using Preact 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
  • 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 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

npm install boardgame-core

Writing a Game

Defining a Game

Each game defines its own context type by extending IGameContext, creates a command registry, and exports helper functions:

import { IGameContext, createGameCommand, createGameContext, createCommandRegistry, registerCommand } from 'boardgame-core';

// 1. Define your game-specific state
type MyGameState = {
    score: number;
    round: number;
};

// 2. Extend IGameContext with your state
type MyGameContext = IGameContext & {
    state: MyGameState;
};

// 3. Define typed commands
const addScore = createGameCommand<MyGameContext, number>(
    'add-score <amount:number>',
    async function(cmd) {
        this.context.state.score += cmd.params[0] as number;
        return this.context.state.score;
    }
);

// 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
export function getScore(ctx: MyGameContext) {
    return ctx.state.score;
}

Running a Game

import { createMyGameContext } from './my-game';

const game = createMyGameContext();

// Run commands through the context
const result = await game.commands.run('add-score 10');
if (result.success) {
    console.log(result.result); // 10
}

// Access game state
console.log(game.state.score); // 10

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.

Boop

A more complex game with piece types (kittens/cats), supply management, the "boop" push mechanic, and graduation rules.

// 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 <from> <to> [--force] [-x: number]');
const result = validateCommand(cmd, schema);
// { valid: true }

Entity Collections

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

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 (parts, regions, commands, prompts)
createGameContext<TContext>(registry, initialState?) Create a game context instance. initialState can be an object or factory function for custom properties
createGameCommand<TContext, TResult>(schema, handler) Create a typed command with access to this.context

Parts

Export Description
Part Entity type representing a game piece
entity(id, data) Create a reactive entity
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
RegionEntity Entity type for spatial grouping of parts
RegionAxis Axis definition with min/max/align
applyAlign(region) Compact parts according to axis alignment
shuffle(region, rng) Randomize part positions
moveToRegion(part, targetRegion, position?) Move a part to another region
moveToRegionAll(parts, targetRegion, positions?) Move multiple parts to another region
removeFromRegion(part) 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<TContext>() 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

Export Description
createEntityCollection<T>() Create a reactive entity collection
createRNG(seed?) Create a seeded RNG instance
Mulberry32RNG Mulberry32 PRNG class

Scripts

npm run build       # Build with tsup
npm run test        # Run tests in watch mode
npm run test:run    # Run tests once
npm run typecheck   # Type check with TypeScript

License

MIT