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,
CommandRegistry,
PromptEvent,
parseCommandSchema,
} from '@/utils/command';
import {createGameCommandRegistry, createGameContext, IGameContext} from './game';
import {createGameCommandRegistry, createGameContext, IGameContext, PromptDef} from './game';
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);
}
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

View File

@ -15,7 +15,7 @@ export interface IGameContext<TState extends Record<string, unknown> = {} > {
produceAsync(fn: (draft: TState) => void): Promise<void>;
run<T>(input: string): 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;
// test only
@ -47,8 +47,8 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
runParsed<T>(command: Command) {
return commands.runParsed<T>(command);
},
prompt(schema, validator, currentPlayer) {
return commands.prompt(schema, validator, currentPlayer);
prompt(def, validator, currentPlayer) {
return commands.prompt(def.schema, validator, currentPlayer);
},
addInterruption(promise) {
state.addInterruption(promise);
@ -63,6 +63,13 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
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> = {} >() {
return createCommandRegistry<IGameContext<TState>>();
}

View File

@ -1,7 +1,7 @@
import {Part} from "./part";
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>> = {};
for (const entry of items) {
const count = getCount ? (typeof getCount === 'function' ? getCount(entry) : getCount) : 1;

View File

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

View File

@ -2,7 +2,7 @@
import {createRegion, moveToRegion, Region} from "@/core/region";
import {createPartsFromTable} from "@/core/part-factory";
import {Part} from "@/core/part";
import {IGameContext} from "@/core/game";
import {createPromptDef, IGameContext} from "@/core/game";
export const BOARD_SIZE = 6;
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 BoopPartMeta = { player: PlayerType; type: PieceType };
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() {
const pieces = createPartsFromTable(

View File

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

View File

@ -51,7 +51,7 @@ describe('createGameContext', () => {
const ctx = createGameContext(registry);
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 => {