refactor: PromptDef

This commit is contained in:
hypercross 2026-04-06 15:47:33 +08:00
parent fe3bef0a01
commit 6cfb3b6df8
7 changed files with 45 additions and 14 deletions

View File

@ -3,8 +3,9 @@ import {
CommandSchema, CommandSchema,
CommandRegistry, CommandRegistry,
PromptEvent, PromptEvent,
parseCommandSchema,
} from '@/utils/command'; } from '@/utils/command';
import {createGameCommandRegistry, createGameContext, IGameContext} from './game'; import {createGameCommandRegistry, createGameContext, IGameContext, PromptDef} from './game';
export type GameHostStatus = 'created' | 'running' | 'disposed'; export type GameHostStatus = 'created' | 'running' | 'disposed';
@ -74,6 +75,16 @@ export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
} }
return this._context._commands._tryCommit(input); return this._context._commands._tryCommit(input);
} }
tryAnswerPrompt<TArgs extends any[]>(def: PromptDef<TArgs>, ...args: TArgs){
if(typeof def.schema === 'string') def.schema = parseCommandSchema(def.schema);
return this._context._commands._tryCommit({
name: def.schema.name,
params: args,
options: {},
flags: {}
});
}
/** /**
* produceAsync Promise UI * produceAsync Promise UI

View File

@ -15,7 +15,7 @@ export interface IGameContext<TState extends Record<string, unknown> = {} > {
produceAsync(fn: (draft: TState) => void): Promise<void>; produceAsync(fn: (draft: TState) => void): Promise<void>;
run<T>(input: string): Promise<CommandResult<T>>; run<T>(input: string): Promise<CommandResult<T>>;
runParsed<T>(command: Command): Promise<CommandResult<T>>; runParsed<T>(command: Command): Promise<CommandResult<T>>;
prompt: <TResult,TArgs extends any[]=any[]>(schema: CommandSchema | string, validator: PromptValidator<TResult,TArgs>, currentPlayer?: string | null) => Promise<TResult>; prompt: <TResult,TArgs extends any[]=any[]>(def: PromptDef<TArgs>, validator: PromptValidator<TResult,TArgs>, currentPlayer?: string | null) => Promise<TResult>;
addInterruption(promise: Promise<void>): void; addInterruption(promise: Promise<void>): void;
// test only // test only
@ -47,8 +47,8 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
runParsed<T>(command: Command) { runParsed<T>(command: Command) {
return commands.runParsed<T>(command); return commands.runParsed<T>(command);
}, },
prompt(schema, validator, currentPlayer) { prompt(def, validator, currentPlayer) {
return commands.prompt(schema, validator, currentPlayer); return commands.prompt(def.schema, validator, currentPlayer);
}, },
addInterruption(promise) { addInterruption(promise) {
state.addInterruption(promise); state.addInterruption(promise);
@ -63,6 +63,13 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
return context; return context;
} }
export type PromptDef<TArgs extends any[]=any[]> = {
schema: CommandSchema | string,
}
export function createPromptDef<TArgs extends any[]=any[]>(schema: CommandSchema | string): PromptDef<TArgs> {
return { schema };
}
export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() { export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() {
return createCommandRegistry<IGameContext<TState>>(); return createCommandRegistry<IGameContext<TState>>();
} }

View File

@ -1,7 +1,7 @@
import {Part} from "./part"; import {Part} from "./part";
import {Immutable} from "mutative"; import {Immutable} from "mutative";
export function createPartsFromTable<T>(items: T[], getId: (item: T, index: number) => string, getCount?: ((item: T) => number) | number){ export function createPartsFromTable<T>(items: readonly T[], getId: (item: T, index: number) => string, getCount?: ((item: T) => number) | number){
const pool: Record<string, Part<T>> = {}; const pool: Record<string, Part<T>> = {};
for (const entry of items) { for (const entry of items) {
const count = getCount ? (typeof getCount === 'function' ? getCount(entry) : getCount) : 1; const count = getCount ? (typeof getCount === 'function' ? getCount(entry) : getCount) : 1;

View File

@ -5,7 +5,9 @@
PlayerType, PlayerType,
WinnerType, WinnerType,
WIN_LENGTH, WIN_LENGTH,
MAX_PIECES_PER_PLAYER, BoopGame MAX_PIECES_PER_PLAYER,
BoopGame,
prompts
} from "./data"; } from "./data";
import {createGameCommandRegistry} from "@/core/game"; import {createGameCommandRegistry} from "@/core/game";
import {moveToRegion} from "@/core/region"; import {moveToRegion} from "@/core/region";
@ -186,8 +188,8 @@ async function handleCheckFullBoard(game: BoopGame, turnPlayer: PlayerType){
} }
const partId = await game.prompt( const partId = await game.prompt(
'choose <player> <row:number> <col:number>', prompts.choose,
(player: PlayerType, row: number, col: number) => { (player, row, col) => {
if (player !== turnPlayer) { if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`; throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
} }
@ -218,8 +220,8 @@ const checkFullBoard = registry.register({
async function handleTurn(game: BoopGame, turnPlayer: PlayerType) { async function handleTurn(game: BoopGame, turnPlayer: PlayerType) {
const {row, col, type} = await game.prompt( const {row, col, type} = await game.prompt(
'play <player> <row:number> <col:number> [type:string]', prompts.play,
(player: PlayerType, row: number, col: number, type?: PieceType) => { (player, row, col, type) => {
const pieceType = type === 'cat' ? 'cat' : 'kitten'; const pieceType = type === 'cat' ? 'cat' : 'kitten';
if (player !== turnPlayer) { if (player !== turnPlayer) {

View File

@ -2,7 +2,7 @@
import {createRegion, moveToRegion, Region} from "@/core/region"; import {createRegion, moveToRegion, Region} from "@/core/region";
import {createPartsFromTable} from "@/core/part-factory"; import {createPartsFromTable} from "@/core/part-factory";
import {Part} from "@/core/part"; import {Part} from "@/core/part";
import {IGameContext} from "@/core/game"; import {createPromptDef, IGameContext} from "@/core/game";
export const BOARD_SIZE = 6; export const BOARD_SIZE = 6;
export const MAX_PIECES_PER_PLAYER = 8; export const MAX_PIECES_PER_PLAYER = 8;
@ -14,6 +14,12 @@ export type WinnerType = PlayerType | 'draw' | null;
export type RegionType = 'white' | 'black' | 'board' | ''; export type RegionType = 'white' | 'black' | 'board' | '';
export type BoopPartMeta = { player: PlayerType; type: PieceType }; export type BoopPartMeta = { player: PlayerType; type: PieceType };
export type BoopPart = Part<BoopPartMeta>; export type BoopPart = Part<BoopPartMeta>;
export const prompts = {
play: createPromptDef<[PlayerType, number, number, PieceType?]>(
'play <player> <row:number> <col:number> [type:string]'),
choose: createPromptDef<[PlayerType, number, number]>(
'choose <player> <row:number> <col:number>')
}
export function createInitialState() { export function createInitialState() {
const pieces = createPartsFromTable( const pieces = createPartsFromTable(

View File

@ -2,6 +2,7 @@ import {
createGameCommandRegistry, Part, createRegion, createGameCommandRegistry, Part, createRegion,
IGameContext IGameContext
} from '@/index'; } from '@/index';
import {createPromptDef} from "@/core/game";
const BOARD_SIZE = 3; const BOARD_SIZE = 3;
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE; const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
@ -35,6 +36,10 @@ export function createInitialState() {
export type TicTacToeState = ReturnType<typeof createInitialState>; export type TicTacToeState = ReturnType<typeof createInitialState>;
export type TicTacToeGame = IGameContext<TicTacToeState>; export type TicTacToeGame = IGameContext<TicTacToeState>;
export const registry = createGameCommandRegistry<TicTacToeState>(); export const registry = createGameCommandRegistry<TicTacToeState>();
export const prompts = {
play: createPromptDef<[PlayerType, number, number]>(
'play <player> <row:number> <col:number>')
}
export async function start(game: TicTacToeGame) { export async function start(game: TicTacToeGame) {
while (true) { while (true) {
@ -59,8 +64,8 @@ const turn = registry.register({
schema: 'turn <player> <turnNumber:number>', schema: 'turn <player> <turnNumber:number>',
async run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) { async run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) {
const {player, row, col} = await game.prompt( const {player, row, col} = await game.prompt(
'play <player> <row:number> <col:number>', prompts.play,
(player: string, row: number, col: number) => { (player, row, col) => {
if (player !== turnPlayer) { if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`; throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
} else if (!isValidMove(row, col)) { } else if (!isValidMove(row, col)) {

View File

@ -51,7 +51,7 @@ describe('createGameContext', () => {
const ctx = createGameContext(registry); const ctx = createGameContext(registry);
registry.register('test <value>', async function (_ctx, value) { registry.register('test <value>', async function (_ctx, value) {
return this.prompt<string>('prompt <answer>', () => 'ok'); return this.prompt({schema: 'prompt <answer>'}, () => 'ok');
}); });
const promptPromise = new Promise<PromptEvent>(resolve => { const promptPromise = new Promise<PromptEvent>(resolve => {