refactor: api change
This commit is contained in:
parent
97ef1df4fb
commit
de7006ef19
|
|
@ -1,9 +1,8 @@
|
||||||
import {MutableSignal, mutableSignal} from "@/utils/mutable-signal";
|
import {MutableSignal, mutableSignal} from "@/utils/mutable-signal";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry,
|
CommandRegistry, CommandResult,
|
||||||
CommandRunnerContext,
|
CommandRunnerContext,
|
||||||
CommandRunnerContextExport,
|
|
||||||
CommandSchema,
|
CommandSchema,
|
||||||
createCommandRegistry,
|
createCommandRegistry,
|
||||||
createCommandRunnerContext,
|
createCommandRunnerContext,
|
||||||
|
|
@ -13,22 +12,57 @@ import {
|
||||||
import type { GameModule } from './game-host';
|
import type { GameModule } from './game-host';
|
||||||
|
|
||||||
export interface IGameContext<TState extends Record<string, unknown> = {} > {
|
export interface IGameContext<TState extends Record<string, unknown> = {} > {
|
||||||
state: MutableSignal<TState>;
|
get value(): TState;
|
||||||
commands: CommandRunnerContextExport<MutableSignal<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> = {} >(
|
export function createGameContext<TState extends Record<string, unknown> = {} >(
|
||||||
commandRegistry: CommandRegistry<MutableSignal<TState>>,
|
commandRegistry: CommandRegistry<IGameContext<TState>>,
|
||||||
initialState?: TState | (() => TState)
|
initialState?: TState | (() => TState)
|
||||||
): IGameContext<TState> {
|
): IGameContext<TState> {
|
||||||
const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState;
|
const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState;
|
||||||
const state = mutableSignal(stateValue);
|
const state = mutableSignal(stateValue);
|
||||||
const commands = createCommandRunnerContext(commandRegistry, state);
|
let commands: CommandRunnerContext<IGameContext<TState>> = null as any;
|
||||||
|
|
||||||
return {
|
const context: IGameContext<TState> = {
|
||||||
state,
|
get value(): TState {
|
||||||
commands
|
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> = {} >(
|
export function createGameContextFromModule<TState extends Record<string, unknown> = {} >(
|
||||||
module: {
|
module: {
|
||||||
registry: CommandRegistry<MutableSignal<TState>>,
|
registry: CommandRegistry<IGameContext<TState>>,
|
||||||
createInitialState: () => TState
|
createInitialState: () => TState
|
||||||
},
|
},
|
||||||
): IGameContext<TState> {
|
): IGameContext<TState> {
|
||||||
|
|
@ -45,27 +79,20 @@ export function createGameContextFromModule<TState extends Record<string, unknow
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() {
|
export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() {
|
||||||
const registry = createCommandRegistry<MutableSignal<TState>>();
|
return createCommandRegistry<IGameContext<TState>>();
|
||||||
return {
|
|
||||||
registry,
|
|
||||||
add<TResult = unknown>(
|
|
||||||
schema: CommandSchema | string,
|
|
||||||
run: (this: CommandRunnerContext<MutableSignal<TState>>, command: Command) => Promise<TResult>
|
|
||||||
){
|
|
||||||
createGameCommand(registry, schema, run);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGameCommand<TState extends Record<string, unknown> = {} , TResult = unknown>(
|
export function registerGameCommand<TState extends Record<string, unknown> = {}, TResult = unknown>(
|
||||||
registry: CommandRegistry<MutableSignal<TState>>,
|
registry: CommandRegistry<IGameContext<TState>>,
|
||||||
schema: CommandSchema | string,
|
schema: CommandSchema | string,
|
||||||
run: (this: CommandRunnerContext<MutableSignal<TState>>, command: Command) => Promise<TResult>
|
run: (this: IGameContext<TState>, ...args: any[]) => Promise<TResult>
|
||||||
) {
|
) {
|
||||||
registerCommand(registry, {
|
registerCommand(registry, {
|
||||||
schema: typeof schema === 'string' ? parseCommandSchema(schema) : schema,
|
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
|
// Core types
|
||||||
export type { IGameContext } from './core/game';
|
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 type { GameHost, GameHostStatus, GameModule } from './core/game';
|
||||||
export { createGameHost, createGameModule } 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 { createPart, createParts, createPartPool, mergePartPools } from './core/part-factory';
|
||||||
|
|
||||||
export type { Region, RegionAxis } from './core/region';
|
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
|
// Utils
|
||||||
export type { Command, CommandResult, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';
|
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 BOARD_SIZE = 3;
|
||||||
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
|
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
|
||||||
|
|
@ -31,33 +34,30 @@ export function createInitialState() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export type TicTacToeState = ReturnType<typeof createInitialState>;
|
export type TicTacToeState = ReturnType<typeof createInitialState>;
|
||||||
const registration = createGameCommandRegistry<TicTacToeState>();
|
export type TicTacToeGame = IGameContext<TicTacToeState>;
|
||||||
export const registry = registration.registry;
|
export const registry = createGameCommandRegistry<TicTacToeState>();
|
||||||
|
|
||||||
registration.add('setup', async function() {
|
registerGameCommand(registry, 'setup', async function() {
|
||||||
const {context} = this;
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const currentPlayer = context.value.currentPlayer;
|
const currentPlayer = this.value.currentPlayer;
|
||||||
const turnNumber = context.value.turn + 1;
|
const turnNumber = this.value.turn + 1;
|
||||||
const turnOutput = await this.run<{winner: WinnerType}>(`turn ${currentPlayer} ${turnNumber}`);
|
const turnOutput = await this.run<{winner: WinnerType}>(`turn ${currentPlayer} ${turnNumber}`);
|
||||||
if (!turnOutput.success) throw new Error(turnOutput.error);
|
if (!turnOutput.success) throw new Error(turnOutput.error);
|
||||||
|
|
||||||
context.produce(state => {
|
this.produce(state => {
|
||||||
state.winner = turnOutput.result.winner;
|
state.winner = turnOutput.result.winner;
|
||||||
if (!state.winner) {
|
if (!state.winner) {
|
||||||
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
|
state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
|
||||||
state.turn = turnNumber;
|
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) {
|
registerGameCommand(registry, 'turn <player:string> <turnNumber:int>', async function turn(turnPlayer: PlayerType, turnNumber: number) {
|
||||||
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
|
|
||||||
|
|
||||||
const playCmd = await this.prompt(
|
const playCmd = await this.prompt(
|
||||||
'play <player> <row:number> <col:number>',
|
'play <player> <row:number> <col:number>',
|
||||||
(command) => {
|
(command) => {
|
||||||
|
|
@ -69,18 +69,18 @@ registration.add('turn <player> <turn:number>', async function(cmd) {
|
||||||
if (!isValidMove(row, col)) {
|
if (!isValidMove(row, col)) {
|
||||||
return `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
|
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 `Cell (${row}, ${col}) is already occupied.`;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
this.context.value.currentPlayer
|
this.value.currentPlayer
|
||||||
);
|
);
|
||||||
const [player, row, col] = playCmd.params as [PlayerType, number, number];
|
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 (winner) return { winner };
|
||||||
if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType };
|
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;
|
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]);
|
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 parts = host.value.parts;
|
||||||
const partsArray = Object.values(parts);
|
const partsArray = Object.values(parts);
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ export function checkWinner(host: MutableSignal<TicTacToeState>): WinnerType {
|
||||||
return null;
|
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 board = host.value.board;
|
||||||
const moveNumber = Object.keys(host.value.parts).length + 1;
|
const moveNumber = Object.keys(host.value.parts).length + 1;
|
||||||
const piece = createPart<{ player: PlayerType }>(
|
const piece = createPart<{ player: PlayerType }>(
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export type CommandResult<T=unknown> = {
|
||||||
export type CommandRunnerContext<TContext> = {
|
export type CommandRunnerContext<TContext> = {
|
||||||
context: TContext;
|
context: TContext;
|
||||||
run: <T=unknown>(input: string) => Promise<CommandResult<T>>;
|
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>;
|
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;
|
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;
|
off: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue