From b1b059de8cf6589a2364f874e93a5261af1a0cd0 Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 3 Apr 2026 14:10:42 +0800 Subject: [PATCH] refactor: improved PromptEvent handling --- README.md | 65 ++++++++++----------------- src/utils/command/command-registry.ts | 17 ++++--- src/utils/command/command-runner.ts | 7 ++- 3 files changed, 39 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index f8ccafd..6c9709c 100644 --- a/README.md +++ b/README.md @@ -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; 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()` | Create a reactive entity collection | | `createRNG(seed?)` | Create a seeded RNG instance | | `Mulberry32RNG` | Mulberry32 PRNG class | diff --git a/src/utils/command/command-registry.ts b/src/utils/command/command-registry.ts index e2361f7..e543283 100644 --- a/src/utils/command/command-registry.ts +++ b/src/utils/command/command-registry.ts @@ -45,7 +45,7 @@ export type CommandRunnerContextExport = CommandRunnerContext; promptQueue: AsyncQueue; _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( 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( ): Promise => { 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) => { diff --git a/src/utils/command/command-runner.ts b/src/utils/command/command-runner.ts index 0f588b2..aa34363 100644 --- a/src/utils/command/command-runner.ts +++ b/src/utils/command/command-runner.ts @@ -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; /** 取消 prompt,Promise 被 reject */ cancel: (reason?: string) => void; };