refactor: update api

This commit is contained in:
hypercross 2026-04-06 10:39:10 +08:00
parent 129c58fb08
commit e673f60657
4 changed files with 45 additions and 47 deletions

View File

@ -4,14 +4,13 @@ import type {
CommandRegistry, CommandRegistry,
PromptEvent, PromptEvent,
CommandRunnerContextExport, CommandRunnerContextExport,
CommandResult
} from '@/utils/command'; } from '@/utils/command';
import type { MutableSignal } from '@/utils/mutable-signal'; import type { MutableSignal } from '@/utils/mutable-signal';
import {createGameContext, IGameContext} from './game'; import {createGameCommandRegistry, createGameContext, IGameContext} from './game';
export type GameHostStatus = 'created' | 'running' | 'disposed'; export type GameHostStatus = 'created' | 'running' | 'disposed';
export class GameHost<TState extends Record<string, unknown>> { export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
readonly context: IGameContext<TState>; readonly context: IGameContext<TState>;
readonly status: ReadonlySignal<GameHostStatus>; readonly status: ReadonlySignal<GameHostStatus>;
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>; readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
@ -19,6 +18,7 @@ export class GameHost<TState extends Record<string, unknown>> {
private _state: MutableSignal<TState>; private _state: MutableSignal<TState>;
private _commands: CommandRunnerContextExport<IGameContext<TState>>; private _commands: CommandRunnerContextExport<IGameContext<TState>>;
private _start: (ctx: IGameContext<TState>) => Promise<TResult>;
private _status: Signal<GameHostStatus>; private _status: Signal<GameHostStatus>;
private _activePromptSchema: Signal<CommandSchema | null>; private _activePromptSchema: Signal<CommandSchema | null>;
private _activePromptPlayer: Signal<string | null>; private _activePromptPlayer: Signal<string | null>;
@ -29,12 +29,14 @@ export class GameHost<TState extends Record<string, unknown>> {
constructor( constructor(
registry: CommandRegistry<IGameContext<TState>>, registry: CommandRegistry<IGameContext<TState>>,
createInitialState: () => TState, createInitialState: () => TState,
start: (ctx: IGameContext<TState>) => Promise<TResult>
) { ) {
this._createInitialState = createInitialState; this._createInitialState = createInitialState;
this._eventListeners = new Map(); this._eventListeners = new Map();
const initialState = createInitialState(); const initialState = createInitialState();
this.context = createGameContext(registry, initialState); this.context = createGameContext(registry, initialState);
this._start = start;
this._status = new Signal<GameHostStatus>('created'); this._status = new Signal<GameHostStatus>('created');
this.status = this._status; this.status = this._status;
@ -94,7 +96,7 @@ export class GameHost<TState extends Record<string, unknown>> {
this._state.clearInterruptions(); this._state.clearInterruptions();
} }
start(startCommand: string): Promise<CommandResult<unknown>> { start(): Promise<TResult> {
if (this._isDisposed) { if (this._isDisposed) {
throw new Error('GameHost is disposed'); throw new Error('GameHost is disposed');
} }
@ -104,7 +106,7 @@ export class GameHost<TState extends Record<string, unknown>> {
const initialState = this._createInitialState(); const initialState = this._createInitialState();
this._state.value = initialState as any; this._state.value = initialState as any;
const promise = this._commands.run(startCommand); const promise = this._start(this.context);
this._status.value = 'running'; this._status.value = 'running';
this._emitEvent('start'); this._emitEvent('start');
@ -147,16 +149,18 @@ export class GameHost<TState extends Record<string, unknown>> {
} }
} }
export type GameModule<TState extends Record<string, unknown>> = { export type GameModule<TState extends Record<string, unknown>, TResult=unknown> = {
registry: CommandRegistry<IGameContext<TState>>; registry?: CommandRegistry<IGameContext<TState>>;
createInitialState: () => TState; createInitialState: () => TState;
start: (ctx: IGameContext<TState>) => Promise<TResult>;
} }
export function createGameHost<TState extends Record<string, unknown>>( export function createGameHost<TState extends Record<string, unknown>>(
gameModule: GameModule<TState> gameModule: GameModule<TState>
): GameHost<TState> { ): GameHost<TState> {
return new GameHost( return new GameHost(
gameModule.registry, gameModule.registry || createGameCommandRegistry(),
gameModule.createInitialState, gameModule.createInitialState,
gameModule.start
); );
} }

View File

@ -159,7 +159,7 @@ const checkGraduates = registry.register({
run: handleCheckGraduates run: handleCheckGraduates
}); });
async function handleStart(game: BoopGame) { export async function start(game: BoopGame) {
while (true) { while (true) {
const currentPlayer = game.value.currentPlayer; const currentPlayer = game.value.currentPlayer;
const turnOutput = await turn(game, currentPlayer); const turnOutput = await turn(game, currentPlayer);
@ -175,10 +175,6 @@ async function handleStart(game: BoopGame) {
return game.value; return game.value;
} }
const start = registry.register({
schema: 'start',
run: handleStart
});
async function handleCheckFullBoard(game: BoopGame, turnPlayer: PlayerType){ async function handleCheckFullBoard(game: BoopGame, turnPlayer: PlayerType){
// 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫 // 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫

View File

@ -36,9 +36,7 @@ export type TicTacToeState = ReturnType<typeof createInitialState>;
export type TicTacToeGame = IGameContext<TicTacToeState>; export type TicTacToeGame = IGameContext<TicTacToeState>;
export const registry = createGameCommandRegistry<TicTacToeState>(); export const registry = createGameCommandRegistry<TicTacToeState>();
registry.register({ export async function start(game: TicTacToeGame) {
schema: 'start',
async run(game: TicTacToeGame) {
while (true) { while (true) {
const currentPlayer = game.value.currentPlayer; const currentPlayer = game.value.currentPlayer;
const turnNumber = game.value.turn + 1; const turnNumber = game.value.turn + 1;
@ -55,8 +53,7 @@ registry.register({
} }
return game.value; return game.value;
} }
});
const turn = registry.register({ const turn = registry.register({
schema: 'turn <player> <turnNumber:number>', schema: 'turn <player> <turnNumber:number>',

View File

@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
import { import {
registry, registry,
createInitialState, createInitialState,
start,
TicTacToeState, TicTacToeState,
WinnerType, WinnerType,
PlayerType PlayerType
@ -12,7 +13,7 @@ import { MutableSignal } from '@/utils/mutable-signal';
function createTestHost() { function createTestHost() {
const host = createGameHost( const host = createGameHost(
{ registry, createInitialState } { registry, createInitialState, start }
); );
return { host }; return { host };
} }
@ -59,7 +60,7 @@ describe('GameHost', () => {
const { host } = createTestHost(); const { host } = createTestHost();
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
const promptEvent = await promptPromise; const promptEvent = await promptPromise;
expect(promptEvent.schema.name).toBe('play'); expect(promptEvent.schema.name).toBe('play');
@ -81,7 +82,7 @@ describe('GameHost', () => {
const { host } = createTestHost(); const { host } = createTestHost();
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
const promptEvent = await promptPromise; const promptEvent = await promptPromise;
@ -106,7 +107,7 @@ describe('GameHost', () => {
const { host } = createTestHost(); const { host } = createTestHost();
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
const promptEvent = await promptPromise; const promptEvent = await promptPromise;
const schema = host.activePromptSchema.value; const schema = host.activePromptSchema.value;
@ -131,7 +132,7 @@ describe('GameHost', () => {
// First setup - make one move // First setup - make one move
let promptPromise = waitForPromptEvent(host); let promptPromise = waitForPromptEvent(host);
let runPromise = host.context._commands.run('start'); let runPromise = host.start();
let promptEvent = await promptPromise; let promptEvent = await promptPromise;
// Make a move // Make a move
@ -150,7 +151,7 @@ describe('GameHost', () => {
const newPromptPromise = waitForPromptEvent(host); const newPromptPromise = waitForPromptEvent(host);
// Reset - should reset state and start new game // Reset - should reset state and start new game
host.start('start'); host.start();
// State should be back to initial // State should be back to initial
expect(host.context._state.value.currentPlayer).toBe('X'); expect(host.context._state.value.currentPlayer).toBe('X');
@ -168,12 +169,12 @@ describe('GameHost', () => {
const { host } = createTestHost(); const { host } = createTestHost();
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
await promptPromise; await promptPromise;
// Setup should cancel the active prompt and reset state // Setup should cancel the active prompt and reset state
host.start('start'); host.start();
// The original runPromise should be rejected due to cancellation // The original runPromise should be rejected due to cancellation
try { try {
@ -192,7 +193,7 @@ describe('GameHost', () => {
const { host } = createTestHost(); const { host } = createTestHost();
host.dispose(); host.dispose();
expect(() => host.start('start')).toThrow('GameHost is disposed'); expect(() => host.start()).toThrow('GameHost is disposed');
}); });
}); });
@ -208,7 +209,7 @@ describe('GameHost', () => {
const { host } = createTestHost(); const { host } = createTestHost();
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
await promptPromise; await promptPromise;
@ -245,7 +246,7 @@ describe('GameHost', () => {
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
// Initial setup via reset // Initial setup via reset
host.start('start'); host.start();
expect(setupCount).toBe(1); expect(setupCount).toBe(1);
// State should be running // State should be running
@ -294,7 +295,7 @@ describe('GameHost', () => {
// Make a move // Make a move
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
const promptEvent = await promptPromise; const promptEvent = await promptPromise;
promptEvent.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); promptEvent.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
@ -320,7 +321,7 @@ describe('GameHost', () => {
// Start a command that triggers prompt // Start a command that triggers prompt
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
await promptPromise; await promptPromise;
@ -369,7 +370,7 @@ describe('GameHost', () => {
}); });
// Start setup command (runs game loop until completion) // Start setup command (runs game loop until completion)
const setupPromise = host.context._commands.run('start'); const setupPromise = host.start();
for (let i = 0; i < moves.length; i++) { for (let i = 0; i < moves.length; i++) {
// Wait until the next prompt event arrives // Wait until the next prompt event arrives
@ -415,7 +416,7 @@ describe('GameHost', () => {
const { host } = createTestHost(); const { host } = createTestHost();
const promptPromise = waitForPromptEvent(host); const promptPromise = waitForPromptEvent(host);
const runPromise = host.context._commands.run('start'); const runPromise = host.start();
const promptEvent = await promptPromise; const promptEvent = await promptPromise;
expect(promptEvent.currentPlayer).toBe('X'); expect(promptEvent.currentPlayer).toBe('X');