refactor: api change
This commit is contained in:
parent
97ef1df4fb
commit
de7006ef19
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 }>(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue