diff --git a/src/core/game.ts b/src/core/game.ts index 722f653..1c3a9ae 100644 --- a/src/core/game.ts +++ b/src/core/game.ts @@ -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 = {} > { - state: MutableSignal; - commands: CommandRunnerContextExport>; + get value(): TState; + produce(fn: (draft: TState) => void): void; + produceAsync(fn: (draft: TState) => void): Promise; + run(input: string): Promise>; + runParsed(command: Command): Promise>; + prompt(schema: CommandSchema | string, validator?: (command: Command) => string | null, currentPlayer?: string | null): Promise; + addInterruption(promise: Promise): void; + + // test only + _state: MutableSignal; + _commands: CommandRunnerContext>; } export function createGameContext = {} >( - commandRegistry: CommandRegistry>, + commandRegistry: CommandRegistry>, initialState?: TState | (() => TState) ): IGameContext { const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState; const state = mutableSignal(stateValue); - const commands = createCommandRunnerContext(commandRegistry, state); + let commands: CommandRunnerContext> = null as any; - return { - state, - commands + const context: IGameContext = { + get value(): TState { + return state.value; + }, + produce(fn) { + return state.produce(fn); + }, + produceAsync(fn) { + return state.produceAsync(fn); + }, + run(input: string) { + return commands.run(input); + }, + runParsed(command: Command) { + return commands.runParsed(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 = {} >( */ export function createGameContextFromModule = {} >( module: { - registry: CommandRegistry>, + registry: CommandRegistry>, createInitialState: () => TState }, ): IGameContext { @@ -45,27 +79,20 @@ export function createGameContextFromModule = {} >() { - const registry = createCommandRegistry>(); - return { - registry, - add( - schema: CommandSchema | string, - run: (this: CommandRunnerContext>, command: Command) => Promise - ){ - createGameCommand(registry, schema, run); - return this; - } - } + return createCommandRegistry>(); } -export function createGameCommand = {} , TResult = unknown>( - registry: CommandRegistry>, +export function registerGameCommand = {}, TResult = unknown>( + registry: CommandRegistry>, schema: CommandSchema | string, - run: (this: CommandRunnerContext>, command: Command) => Promise + run: (this: IGameContext, ...args: any[]) => Promise ) { registerCommand(registry, { schema: typeof schema === 'string' ? parseCommandSchema(schema) : schema, - run, + async run(this: CommandRunnerContext>, command: Command){ + const params = command.params; + return await run.call(this.context, ...params); + }, }); } diff --git a/src/index.ts b/src/index.ts index f7b2499..d8f4dd4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/samples/tic-tac-toe.ts b/src/samples/tic-tac-toe.ts index 8231889..13f859e 100644 --- a/src/samples/tic-tac-toe.ts +++ b/src/samples/tic-tac-toe.ts @@ -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; -const registration = createGameCommandRegistry(); -export const registry = registration.registry; +export type TicTacToeGame = IGameContext; +export const registry = createGameCommandRegistry(); -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 ', async function(cmd) { - const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number]; - +registerGameCommand(registry, 'turn ', async function turn(turnPlayer: PlayerType, turnNumber: number) { const playCmd = await this.prompt( 'play ', (command) => { @@ -69,18 +69,18 @@ registration.add('turn ', 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, 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): 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): WinnerType { return null; } -export function placePiece(host: MutableSignal, 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 }>( diff --git a/src/utils/command/command-runner.ts b/src/utils/command/command-runner.ts index 82b514d..84c0682 100644 --- a/src/utils/command/command-runner.ts +++ b/src/utils/command/command-runner.ts @@ -34,7 +34,7 @@ export type CommandResult = { export type CommandRunnerContext = { context: TContext; run: (input: string) => Promise>; - runParsed: (command: Command) => Promise<{ success: true; result: unknown } | { success: false; error: string }>; + runParsed: (command: Command) => Promise>; prompt: (schema: CommandSchema | string, validator?: (command: Command) => string | null, currentPlayer?: string | null) => Promise; on: (event: T, listener: (e: CommandRunnerEvents[T]) => void) => void; off: (event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;