refactor: update api
This commit is contained in:
parent
129c58fb08
commit
e673f60657
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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个棋子都在棋盘上且没有获胜,强制升级一个小猫
|
||||||
|
|
|
||||||
|
|
@ -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>',
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue