187 lines
5.0 KiB
Markdown
187 lines
5.0 KiB
Markdown
# boardgame-core
|
|
|
|
A state management library for board games using [Preact Signals](https://preactjs.com/guide/v10/signals/).
|
|
|
|
## 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
|
|
- **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
|
|
- **Deterministic RNG**: Seeded pseudo-random number generator (Mulberry32) for reproducible game states
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install boardgame-core
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Game Context
|
|
|
|
```ts
|
|
import { createGameContext } from 'boardgame-core';
|
|
|
|
const game = createGameContext({ type: 'game' });
|
|
|
|
// Access entity collections
|
|
game.parts.add({ id: 'card1', sides: 2, side: 0, region: /* ... */, position: [0] });
|
|
game.regions.add({ id: 'hand', axes: [{ name: 'slot', min: 0, max: 7, align: 'start' }], children: [] });
|
|
|
|
// Context stack for nested rule scopes
|
|
game.pushContext({ type: 'combat' });
|
|
const combatCtx = game.latestContext('combat');
|
|
game.popContext();
|
|
```
|
|
|
|
### Parts (Cards, Dice, Tokens)
|
|
|
|
```ts
|
|
import { flip, flipTo, roll, createRNG } from 'boardgame-core';
|
|
|
|
const rng = createRNG(42);
|
|
|
|
flip(card); // cycle to next side
|
|
flipTo(card, 0); // set to specific side
|
|
roll(dice, rng); // random side using seeded RNG
|
|
```
|
|
|
|
### Regions & Alignment
|
|
|
|
```ts
|
|
import { applyAlign, shuffle } from 'boardgame-core';
|
|
|
|
// 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
|
|
|
|
```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');
|
|
```
|
|
|
|
### Rule Engine
|
|
|
|
```ts
|
|
import { createRule, invokeRuleContext } from 'boardgame-core';
|
|
|
|
const myRule = createRule('drawCard', (ctx) => {
|
|
// yield action types to pause and wait for external handling
|
|
const action = yield 'draw';
|
|
ctx.resolution = action;
|
|
});
|
|
|
|
const result = invokeRuleContext(
|
|
game.pushContext.bind(game),
|
|
'drawCard',
|
|
myRule({ type: 'drawCard', actions: [], handledActions: 0, invocations: [] })
|
|
);
|
|
```
|
|
|
|
### 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 |
|
|
|---|---|
|
|
| `createGameContext(root?)` | Create a new game context instance |
|
|
| `GameContext` | The game context model class |
|
|
| `Context` | Base context type |
|
|
|
|
### Parts
|
|
|
|
| Export | Description |
|
|
|---|---|
|
|
| `Part` | Entity type representing a game piece (card, die, token, etc.) |
|
|
| `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` | 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 |
|
|
|
|
### Rules
|
|
|
|
| Export | Description |
|
|
|---|---|
|
|
| `RuleContext<T>` | Rule execution context type |
|
|
| `createRule(type, fn)` | Create a rule generator factory |
|
|
| `invokeRuleContext(pushContext, type, rule)` | Execute a rule with context management |
|
|
|
|
### 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 |
|
|
|
|
### Utilities
|
|
|
|
| Export | Description |
|
|
|---|---|
|
|
| `createEntityCollection<T>()` | Create a reactive entity collection |
|
|
| `createRNG(seed?)` | Create a seeded RNG instance |
|
|
| `Mulberry32RNG` | Mulberry32 PRNG class |
|
|
|
|
## Scripts
|
|
|
|
```bash
|
|
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
|