refactor: improved PromptEvent handling

This commit is contained in:
hypercross 2026-04-03 14:10:42 +08:00
parent 8b2a8888d3
commit b1b059de8c
3 changed files with 39 additions and 50 deletions

View File

@ -8,7 +8,6 @@ Build turn-based board games with reactive state, entity collections, spatial re
- **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 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
@ -28,13 +27,14 @@ Each game defines a command registry and exports a `createInitialState` function
```ts
import {
createGameCommandRegistry,
RegionEntity,
Entity,
Region,
createRegion,
} from 'boardgame-core';
// 1. Define your game-specific state
type MyGameState = {
board: RegionEntity;
board: Region;
parts: Record<string, { id: string; regionId: string; position: number[] }>;
score: { white: number; black: number };
currentPlayer: 'white' | 'black';
winner: 'white' | 'black' | 'draw' | null;
@ -43,14 +43,11 @@ type MyGameState = {
// 2. Create initial state factory
export function createInitialState(): MyGameState {
return {
board: new RegionEntity('board', {
id: 'board',
axes: [
{ name: 'x', min: 0, max: 5 },
{ name: 'y', min: 0, max: 5 },
],
children: [],
}),
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,
@ -110,12 +107,7 @@ 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: {},
});
const error = promptEvent.tryCommit('play X 1 2');
if (error) {
console.log('Invalid move:', error);
@ -149,13 +141,16 @@ See [`src/samples/boop/index.ts`](src/samples/boop/index.ts).
## Region System
```ts
import { RegionEntity, applyAlign, shuffle } from 'boardgame-core';
import { applyAlign, shuffle, moveToRegion } from 'boardgame-core';
// Compact cards in a hand towards the start
applyAlign(handRegion);
applyAlign(handRegion, parts);
// Shuffle positions of all parts in a region
shuffle(handRegion, rng);
shuffle(handRegion, parts, rng);
// Move a part from one region to another
moveToRegion(part, sourceRegion, targetRegion, [0, 0]);
```
## Command Parsing
@ -173,20 +168,6 @@ 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
@ -223,13 +204,14 @@ rng.setSeed(999); // reseed
| Export | Description |
|---|---|
| `RegionEntity` | Entity type for spatial grouping of parts with axis-based positioning |
| `Region` | Type for spatial grouping of parts with axis-based positioning |
| `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 |
| `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 |
| `moveToRegionAll(parts, sourceRegion, targetRegion, positions?)` | Move multiple parts to another region |
| `removeFromRegion(part, region)` | Remove a part from its region |
### Commands
@ -254,7 +236,6 @@ rng.setSeed(999); // reseed
| Export | Description |
|---|---|
| `createEntityCollection<T>()` | Create a reactive entity collection |
| `createRNG(seed?)` | Create a seeded RNG instance |
| `Mulberry32RNG` | Mulberry32 PRNG class |

View File

@ -45,7 +45,7 @@ export type CommandRunnerContextExport<TContext> = CommandRunnerContext<TContext
registry: CommandRegistry<TContext>;
promptQueue: AsyncQueue<PromptEvent>;
_activePrompt: PromptEvent | null;
_tryCommit: (command: Command) => string | null;
_tryCommit: (commandOrInput: Command | string) => string | null;
_cancel: (reason?: string) => void;
_pendingInput: string | null;
};
@ -66,9 +66,9 @@ export function createCommandRunnerContext<TContext>(
let activePrompt: PromptEvent | null = null;
const tryCommit = (command: Command) => {
const tryCommit = (commandOrInput: Command | string) => {
if (activePrompt) {
const result = activePrompt.tryCommit(command);
const result = activePrompt.tryCommit(commandOrInput);
if (result === null) {
activePrompt = null;
}
@ -90,10 +90,15 @@ export function createCommandRunnerContext<TContext>(
): Promise<Command> => {
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
return new Promise((resolve, reject) => {
const tryCommit = (command: Command) => {
const error = validator?.(command);
const tryCommit = (commandOrInput: Command | string) => {
const command = typeof commandOrInput === 'string' ? parseCommand(commandOrInput) : commandOrInput;
const schemaResult = applyCommandSchema(command, resolvedSchema);
if (!schemaResult.valid) {
return schemaResult.errors.join('; ');
}
const error = validator?.(schemaResult.command);
if (error) return error;
resolve(command);
resolve(schemaResult.command);
return null;
};
const cancel = (reason?: string) => {

View File

@ -1,13 +1,16 @@
import type { Command, CommandSchema } from './types';
import { parseCommand } from './command-parse';
import { applyCommandSchema } from './command-validate';
export type PromptEvent = {
schema: CommandSchema;
/**
/**
*
* @param commandOrInput Command
* @returns null - Promise resolve
* @returns string - Promise resolve
*/
tryCommit: (command: Command) => string | null;
tryCommit: (commandOrInput: Command | string) => string | null;
/** 取消 promptPromise 被 reject */
cancel: (reason?: string) => void;
};