refactor: api change

This commit is contained in:
hypercross 2026-04-04 18:29:33 +08:00
parent 97ef1df4fb
commit de7006ef19
4 changed files with 75 additions and 48 deletions

View File

@ -1,9 +1,8 @@
import {MutableSignal, mutableSignal} from "@/utils/mutable-signal";
import {
Command,
CommandRegistry,
CommandRegistry, CommandResult,
CommandRunnerContext,
CommandRunnerContextExport,
CommandSchema,
createCommandRegistry,
createCommandRunnerContext,
@ -13,22 +12,57 @@ import {
import type { GameModule } from './game-host';
export interface IGameContext<TState extends Record<string, unknown> = {} > {
state: MutableSignal<TState>;
commands: CommandRunnerContextExport<MutableSignal<TState>>;
get value(): TState;
produce(fn: (draft: TState) => void): void;
produceAsync(fn: (draft: TState) => void): Promise<void>;
run<T>(input: string): Promise<CommandResult<T>>;
runParsed<T>(command: Command): Promise<CommandResult<T>>;
prompt(schema: CommandSchema | string, validator?: (command: Command) => string | null, currentPlayer?: string | null): Promise<Command>;
addInterruption(promise: Promise<void>): void;
// test only
_state: MutableSignal<TState>;
_commands: CommandRunnerContext<IGameContext<TState>>;
}
export function createGameContext<TState extends Record<string, unknown> = {} >(
commandRegistry: CommandRegistry<MutableSignal<TState>>,
commandRegistry: CommandRegistry<IGameContext<TState>>,
initialState?: TState | (() => TState)
): IGameContext<TState> {
const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState;
const state = mutableSignal(stateValue);
const commands = createCommandRunnerContext(commandRegistry, state);
let commands: CommandRunnerContext<IGameContext<TState>> = null as any;
return {
state,
commands
const context: IGameContext<TState> = {
get value(): TState {
return state.value;
},
produce(fn) {
return state.produce(fn);
},
produceAsync(fn) {
return state.produceAsync(fn);
},
run<T>(input: string) {
return commands.run<T>(input);
},
runParsed<T>(command: Command) {
return commands.runParsed<T>(command);
},
prompt(schema, validator, currentPlayer) {
return commands.prompt(schema, validator, currentPlayer);
},
addInterruption(promise) {
state.addInterruption(promise);
},
_state: state,
_commands: commands,
};
context._commands = commands = createCommandRunnerContext(commandRegistry, context);
return context;
}
/**
@ -37,7 +71,7 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
*/
export function createGameContextFromModule<TState extends Record<string, unknown> = {} >(
module: {
registry: CommandRegistry<MutableSignal<TState>>,
registry: CommandRegistry<IGameContext<TState>>,
createInitialState: () => TState
},
): IGameContext<TState> {
@ -45,27 +79,20 @@ export function createGameContextFromModule<TState extends Record<string, unknow
}
export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() {
const registry = createCommandRegistry<MutableSignal<TState>>();
return {
registry,
add<TResult = unknown>(
schema: CommandSchema | string,
run: (this: CommandRunnerContext<MutableSignal<TState>>, command: Command) => Promise<TResult>
){
createGameCommand(registry, schema, run);
return this;
}
}
return createCommandRegistry<IGameContext<TState>>();
}
export function createGameCommand<TState extends Record<string, unknown> = {} , TResult = unknown>(
registry: CommandRegistry<MutableSignal<TState>>,
export function registerGameCommand<TState extends Record<string, unknown> = {}, TResult = unknown>(
registry: CommandRegistry<IGameContext<TState>>,
schema: CommandSchema | string,
run: (this: CommandRunnerContext<MutableSignal<TState>>, command: Command) => Promise<TResult>
run: (this: IGameContext<TState>, ...args: any[]) => Promise<TResult>
) {
registerCommand(registry, {
schema: typeof schema === 'string' ? parseCommandSchema(schema) : schema,
run,
async run(this: CommandRunnerContext<IGameContext<TState>>, command: Command){
const params = command.params;
return await run.call(this.context, ...params);
},
});
}

View File

@ -5,7 +5,7 @@
// Core types
export type { IGameContext } from './core/game';
export { createGameContext, createGameCommandRegistry } from './core/game';
export { createGameContext, createGameCommandRegistry, registerGameCommand } from './core/game';
export type { GameHost, GameHostStatus, GameModule } from './core/game';
export { createGameHost, createGameModule } from './core/game';
@ -17,7 +17,7 @@ export type { PartTemplate, PartPool } from './core/part-factory';
export { createPart, createParts, createPartPool, mergePartPools } from './core/part-factory';
export type { Region, RegionAxis } from './core/region';
export { createRegion, applyAlign, shuffle, moveToRegion, moveToRegionAll } from './core/region';
export { createRegion, applyAlign, shuffle, moveToRegion } from './core/region';
// Utils
export type { Command, CommandResult, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';

View File

@ -1,4 +1,7 @@
import {createGameCommandRegistry, Part, MutableSignal, createRegion, createPart, isCellOccupied as isCellOccupiedUtil} from '@/index';
import {
createGameCommandRegistry, Part, createRegion, createPart, isCellOccupied as isCellOccupiedUtil,
registerGameCommand, IGameContext
} from '@/index';
const BOARD_SIZE = 3;
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
@ -31,33 +34,30 @@ export function createInitialState() {
};
}
export type TicTacToeState = ReturnType<typeof createInitialState>;
const registration = createGameCommandRegistry<TicTacToeState>();
export const registry = registration.registry;
export type TicTacToeGame = IGameContext<TicTacToeState>;
export const registry = createGameCommandRegistry<TicTacToeState>();
registration.add('setup', async function() {
const {context} = this;
registerGameCommand(registry, 'setup', async function() {
while (true) {
const currentPlayer = context.value.currentPlayer;
const turnNumber = context.value.turn + 1;
const currentPlayer = this.value.currentPlayer;
const turnNumber = this.value.turn + 1;
const turnOutput = await this.run<{winner: WinnerType}>(`turn ${currentPlayer} ${turnNumber}`);
if (!turnOutput.success) throw new Error(turnOutput.error);
context.produce(state => {
this.produce(state => {
state.winner = turnOutput.result.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
state.turn = turnNumber;
}
});
if (context.value.winner) break;
if (this.value.winner) break;
}
return context.value;
return this.value;
});
registration.add('turn <player> <turn:number>', async function(cmd) {
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
registerGameCommand(registry, 'turn <player:string> <turnNumber:int>', async function turn(turnPlayer: PlayerType, turnNumber: number) {
const playCmd = await this.prompt(
'play <player> <row:number> <col:number>',
(command) => {
@ -69,18 +69,18 @@ registration.add('turn <player> <turn:number>', async function(cmd) {
if (!isValidMove(row, col)) {
return `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
}
if (isCellOccupied(this.context, row, col)) {
if (isCellOccupied(this, row, col)) {
return `Cell (${row}, ${col}) is already occupied.`;
}
return null;
},
this.context.value.currentPlayer
this.value.currentPlayer
);
const [player, row, col] = playCmd.params as [PlayerType, number, number];
placePiece(this.context, row, col, turnPlayer);
placePiece(this, row, col, turnPlayer);
const winner = checkWinner(this.context);
const winner = checkWinner(this);
if (winner) return { winner };
if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType };
@ -91,7 +91,7 @@ function isValidMove(row: number, col: number): boolean {
return !isNaN(row) && !isNaN(col) && row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
}
export function isCellOccupied(host: MutableSignal<TicTacToeState>, row: number, col: number): boolean {
export function isCellOccupied(host: TicTacToeGame, row: number, col: number): boolean {
return isCellOccupiedUtil(host.value.parts, 'board', [row, col]);
}
@ -103,7 +103,7 @@ export function hasWinningLine(positions: number[][]): boolean {
);
}
export function checkWinner(host: MutableSignal<TicTacToeState>): WinnerType {
export function checkWinner(host: TicTacToeGame): WinnerType {
const parts = host.value.parts;
const partsArray = Object.values(parts);
@ -117,7 +117,7 @@ export function checkWinner(host: MutableSignal<TicTacToeState>): WinnerType {
return null;
}
export function placePiece(host: MutableSignal<TicTacToeState>, row: number, col: number, player: PlayerType) {
export function placePiece(host: TicTacToeGame, row: number, col: number, player: PlayerType) {
const board = host.value.board;
const moveNumber = Object.keys(host.value.parts).length + 1;
const piece = createPart<{ player: PlayerType }>(

View File

@ -34,7 +34,7 @@ export type CommandResult<T=unknown> = {
export type CommandRunnerContext<TContext> = {
context: TContext;
run: <T=unknown>(input: string) => Promise<CommandResult<T>>;
runParsed: (command: Command) => Promise<{ success: true; result: unknown } | { success: false; error: string }>;
runParsed: <T=unknown>(command: Command) => Promise<CommandResult<T>>;
prompt: (schema: CommandSchema | string, validator?: (command: Command) => string | null, currentPlayer?: string | null) => Promise<Command>;
on: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;
off: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;