refactor: improved PromptEvent handling
This commit is contained in:
parent
8b2a8888d3
commit
b1b059de8c
61
README.md
61
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/)
|
- **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 |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
/** 取消 prompt,Promise 被 reject */
|
/** 取消 prompt,Promise 被 reject */
|
||||||
cancel: (reason?: string) => void;
|
cancel: (reason?: string) => void;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue