refactor: improved PromptEvent handling
This commit is contained in:
parent
8b2a8888d3
commit
b1b059de8c
65
README.md
65
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<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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue