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/) - **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 - **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 - **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 - **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 - **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 ```ts
import { import {
createGameCommandRegistry, createGameCommandRegistry,
RegionEntity, Region,
Entity, createRegion,
} from 'boardgame-core'; } from 'boardgame-core';
// 1. Define your game-specific state // 1. Define your game-specific state
type MyGameState = { type MyGameState = {
board: RegionEntity; board: Region;
parts: Record<string, { id: string; regionId: string; position: number[] }>;
score: { white: number; black: number }; score: { white: number; black: number };
currentPlayer: 'white' | 'black'; currentPlayer: 'white' | 'black';
winner: 'white' | 'black' | 'draw' | null; winner: 'white' | 'black' | 'draw' | null;
@ -43,14 +43,11 @@ type MyGameState = {
// 2. Create initial state factory // 2. Create initial state factory
export function createInitialState(): MyGameState { export function createInitialState(): MyGameState {
return { return {
board: new RegionEntity('board', { board: createRegion('board', [
id: 'board',
axes: [
{ name: 'x', min: 0, max: 5 }, { name: 'x', min: 0, max: 5 },
{ name: 'y', min: 0, max: 5 }, { name: 'y', min: 0, max: 5 },
], ]),
children: [], parts: {},
}),
score: { white: 0, black: 0 }, score: { white: 0, black: 0 },
currentPlayer: 'white', currentPlayer: 'white',
winner: null, winner: null,
@ -110,12 +107,7 @@ const promptEvent = await game.commands.promptQueue.pop();
console.log(promptEvent.schema.name); // e.g. 'play' console.log(promptEvent.schema.name); // e.g. 'play'
// Validate and submit player input // Validate and submit player input
const error = promptEvent.tryCommit({ const error = promptEvent.tryCommit('play X 1 2');
name: 'play',
params: ['X', 1, 1],
options: {},
flags: {},
});
if (error) { if (error) {
console.log('Invalid move:', error); console.log('Invalid move:', error);
@ -149,13 +141,16 @@ See [`src/samples/boop/index.ts`](src/samples/boop/index.ts).
## Region System ## Region System
```ts ```ts
import { RegionEntity, applyAlign, shuffle } from 'boardgame-core'; import { applyAlign, shuffle, moveToRegion } from 'boardgame-core';
// Compact cards in a hand towards the start // Compact cards in a hand towards the start
applyAlign(handRegion); applyAlign(handRegion, parts);
// Shuffle positions of all parts in a region // 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 ## Command Parsing
@ -173,20 +168,6 @@ const result = validateCommand(cmd, schema);
// { valid: true } // { 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 ## Random Number Generation
```ts ```ts
@ -223,13 +204,14 @@ rng.setSeed(999); // reseed
| Export | Description | | 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 | | `RegionAxis` | Axis definition with min/max/align |
| `applyAlign(region)` | Compact parts according to axis alignment | | `createRegion(id, axes)` | Create a new region |
| `shuffle(region, rng)` | Randomize part positions | | `applyAlign(region, parts)` | Compact parts according to axis alignment |
| `moveToRegion(part, targetRegion, position?)` | Move a part to another region | | `shuffle(region, parts, rng)` | Randomize part positions |
| `moveToRegionAll(parts, targetRegion, positions?)` | Move multiple parts to another region | | `moveToRegion(part, sourceRegion, targetRegion, position?)` | Move a part to another region |
| `removeFromRegion(part)` | Remove a part from its region | | `moveToRegionAll(parts, sourceRegion, targetRegion, positions?)` | Move multiple parts to another region |
| `removeFromRegion(part, region)` | Remove a part from its region |
### Commands ### Commands
@ -254,7 +236,6 @@ rng.setSeed(999); // reseed
| Export | Description | | Export | Description |
|---|---| |---|---|
| `createEntityCollection<T>()` | Create a reactive entity collection |
| `createRNG(seed?)` | Create a seeded RNG instance | | `createRNG(seed?)` | Create a seeded RNG instance |
| `Mulberry32RNG` | Mulberry32 PRNG class | | `Mulberry32RNG` | Mulberry32 PRNG class |

View File

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

View File

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