Compare commits
No commits in common. "ae98e0735a514a208d1fcbd572156a128b416063" and "d1264f3b9e22fbad8e8e92c4f59b122fbffec1d9" have entirely different histories.
ae98e0735a
...
d1264f3b9e
|
|
@ -1,5 +1,5 @@
|
|||
import { ReadonlySignal, signal, Signal } from '@preact/signals-core';
|
||||
import type { CommandSchema, CommandRegistry, CommandResult, PromptEvent } from '@/utils/command';
|
||||
import type { CommandSchema, CommandRegistry, CommandResult } from '@/utils/command';
|
||||
import type { MutableSignal } from '@/utils/mutable-signal';
|
||||
import { createGameContext } from './game';
|
||||
|
||||
|
|
@ -9,22 +9,15 @@ export interface GameHostOptions {
|
|||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
export interface GameModule<TState extends Record<string, unknown>> {
|
||||
registry: CommandRegistry<MutableSignal<TState>>;
|
||||
createInitialState: () => TState;
|
||||
}
|
||||
|
||||
export class GameHost<TState extends Record<string, unknown>> {
|
||||
readonly state: ReadonlySignal<TState>;
|
||||
readonly commands: ReturnType<typeof createGameContext<TState>>['commands'];
|
||||
readonly status: ReadonlySignal<GameHostStatus>;
|
||||
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
|
||||
readonly activePromptPlayer: ReadonlySignal<string | null>;
|
||||
|
||||
private _state: MutableSignal<TState>;
|
||||
private _status: Signal<GameHostStatus>;
|
||||
private _activePromptSchema: Signal<CommandSchema | null>;
|
||||
private _activePromptPlayer: Signal<string | null>;
|
||||
private _createInitialState: () => TState;
|
||||
private _setupCommand: string;
|
||||
private _eventListeners: Map<'setup' | 'dispose', Set<() => void>>;
|
||||
|
|
@ -52,9 +45,6 @@ export class GameHost<TState extends Record<string, unknown>> {
|
|||
this._activePromptSchema = new Signal<CommandSchema | null>(null);
|
||||
this.activePromptSchema = this._activePromptSchema;
|
||||
|
||||
this._activePromptPlayer = new Signal<string | null>(null);
|
||||
this.activePromptPlayer = this._activePromptPlayer;
|
||||
|
||||
this.state = this._state;
|
||||
|
||||
this._setupPromptTracking();
|
||||
|
|
@ -65,23 +55,20 @@ export class GameHost<TState extends Record<string, unknown>> {
|
|||
}
|
||||
|
||||
private _setupPromptTracking() {
|
||||
let currentPromptEvent: PromptEvent | null = null;
|
||||
const updateSchema = () => {
|
||||
const activePrompt = (this.commands as any)._activePrompt as { schema?: CommandSchema } | null;
|
||||
this._activePromptSchema.value = activePrompt?.schema ?? null;
|
||||
};
|
||||
|
||||
this.commands.on('prompt', (e) => {
|
||||
currentPromptEvent = e as PromptEvent;
|
||||
this._activePromptSchema.value = currentPromptEvent.schema;
|
||||
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
|
||||
this.commands.on('prompt', () => {
|
||||
updateSchema();
|
||||
});
|
||||
|
||||
this.commands.on('promptEnd', () => {
|
||||
currentPromptEvent = null;
|
||||
this._activePromptSchema.value = null;
|
||||
this._activePromptPlayer.value = null;
|
||||
updateSchema();
|
||||
});
|
||||
|
||||
// Initial state
|
||||
this._activePromptSchema.value = null;
|
||||
this._activePromptPlayer.value = null;
|
||||
updateSchema();
|
||||
}
|
||||
|
||||
onInput(input: string): string | null {
|
||||
|
|
@ -147,7 +134,10 @@ export class GameHost<TState extends Record<string, unknown>> {
|
|||
}
|
||||
|
||||
export function createGameHost<TState extends Record<string, unknown>>(
|
||||
module: GameModule<TState>,
|
||||
module: {
|
||||
registry: CommandRegistry<MutableSignal<TState>>;
|
||||
createInitialState: () => TState;
|
||||
},
|
||||
setupCommand: string,
|
||||
options?: GameHostOptions
|
||||
): GameHost<TState> {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
parseCommandSchema,
|
||||
registerCommand
|
||||
} from "@/utils/command";
|
||||
import type { GameModule } from './game-host';
|
||||
|
||||
export interface IGameContext<TState extends Record<string, unknown> = {} > {
|
||||
state: MutableSignal<TState>;
|
||||
|
|
@ -70,10 +69,4 @@ export function createGameCommand<TState extends Record<string, unknown> = {} ,
|
|||
}
|
||||
|
||||
export { GameHost, createGameHost } from './game-host';
|
||||
export type { GameHostStatus, GameHostOptions, GameModule } from './game-host';
|
||||
|
||||
export function createGameModule<TState extends Record<string, unknown>>(
|
||||
module: GameModule<TState>
|
||||
): GameModule<TState> {
|
||||
return module;
|
||||
}
|
||||
export type { GameHostStatus, GameHostOptions } from './game-host';
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
export type { IGameContext } from './core/game';
|
||||
export { createGameContext, createGameCommandRegistry } from './core/game';
|
||||
|
||||
export type { GameHost, GameHostStatus, GameHostOptions, GameModule } from './core/game';
|
||||
export { createGameHost, createGameModule } from './core/game';
|
||||
export type { GameHost, GameHostStatus, GameHostOptions } from './core/game-host';
|
||||
export { createGameHost } from './core/game-host';
|
||||
|
||||
export type { Part } from './core/part';
|
||||
export { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion } from './core/part';
|
||||
|
|
|
|||
|
|
@ -113,8 +113,7 @@ registration.add('turn <player>', async function(cmd) {
|
|||
return `No ${pieceType}s left in ${player}'s supply.`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
this.context.value.currentPlayer
|
||||
}
|
||||
);
|
||||
const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?];
|
||||
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
||||
|
|
@ -142,8 +141,7 @@ registration.add('turn <player>', async function(cmd) {
|
|||
const part = availableKittens.find(p => `${p.position[0]},${p.position[1]}` === posKey);
|
||||
if (!part) return `No kitten at (${row}, ${col}).`;
|
||||
return null;
|
||||
},
|
||||
this.context.value.currentPlayer
|
||||
}
|
||||
);
|
||||
const [row, col] = graduateCmd.params as [number, number];
|
||||
const part = availableKittens.find(p => p.position[0] === row && p.position[1] === col)!;
|
||||
|
|
|
|||
|
|
@ -73,8 +73,7 @@ registration.add('turn <player> <turn:number>', async function(cmd) {
|
|||
return `Cell (${row}, ${col}) is already occupied.`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
this.context.value.currentPlayer
|
||||
}
|
||||
);
|
||||
const [player, row, col] = playCmd.params as [PlayerType, number, number];
|
||||
|
||||
|
|
|
|||
|
|
@ -104,8 +104,7 @@ export function createCommandRunnerContext<TContext>(
|
|||
|
||||
const prompt = (
|
||||
schema: CommandSchema | string,
|
||||
validator?: (command: Command) => string | null,
|
||||
currentPlayer?: string | null
|
||||
validator?: (command: Command) => string | null
|
||||
): Promise<Command> => {
|
||||
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -121,12 +120,10 @@ export function createCommandRunnerContext<TContext>(
|
|||
return null;
|
||||
};
|
||||
const cancel = (reason?: string) => {
|
||||
activePrompt = null;
|
||||
emitPromptEnd();
|
||||
reject(new Error(reason ?? 'Cancelled'));
|
||||
};
|
||||
activePrompt = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel };
|
||||
const event: PromptEvent = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel };
|
||||
activePrompt = { schema: resolvedSchema, tryCommit, cancel };
|
||||
const event: PromptEvent = { schema: resolvedSchema, tryCommit, cancel };
|
||||
for (const listener of promptListeners) {
|
||||
listener(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { applyCommandSchema } from './command-validate';
|
|||
|
||||
export type PromptEvent = {
|
||||
schema: CommandSchema;
|
||||
/** 当前等待输入的玩家 */
|
||||
currentPlayer: string | null;
|
||||
/**
|
||||
* 尝试提交命令
|
||||
* @param commandOrInput Command 对象或命令字符串
|
||||
|
|
@ -35,7 +33,7 @@ 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 }>;
|
||||
prompt: (schema: CommandSchema | string, validator?: (command: Command) => string | null, currentPlayer?: string | null) => Promise<Command>;
|
||||
prompt: (schema: CommandSchema | string, validator?: (command: Command) => 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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -410,50 +410,4 @@ describe('GameHost', () => {
|
|||
expect(host.status.value).toBe('disposed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentPlayer in prompt', () => {
|
||||
it('should have currentPlayer in PromptEvent', async () => {
|
||||
const { host } = createTestHost();
|
||||
|
||||
const promptPromise = waitForPromptEvent(host);
|
||||
const runPromise = host.commands.run('setup');
|
||||
|
||||
const promptEvent = await promptPromise;
|
||||
expect(promptEvent.currentPlayer).toBe('X');
|
||||
expect(host.activePromptPlayer.value).toBe('X');
|
||||
|
||||
promptEvent.cancel('test cleanup');
|
||||
await runPromise;
|
||||
});
|
||||
|
||||
it('should update activePromptPlayer reactively', async () => {
|
||||
const { host } = createTestHost();
|
||||
|
||||
// Initially null
|
||||
expect(host.activePromptPlayer.value).toBeNull();
|
||||
|
||||
// First prompt - X's turn
|
||||
let promptPromise = waitForPromptEvent(host);
|
||||
let runPromise = host.commands.run('setup');
|
||||
let promptEvent = await promptPromise;
|
||||
expect(promptEvent.currentPlayer).toBe('X');
|
||||
expect(host.activePromptPlayer.value).toBe('X');
|
||||
|
||||
// Make a move
|
||||
promptEvent.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
||||
|
||||
// Second prompt - O's turn
|
||||
promptPromise = waitForPromptEvent(host);
|
||||
promptEvent = await promptPromise;
|
||||
expect(promptEvent.currentPlayer).toBe('O');
|
||||
expect(host.activePromptPlayer.value).toBe('O');
|
||||
|
||||
// Cancel
|
||||
promptEvent.cancel('test cleanup');
|
||||
await runPromise;
|
||||
|
||||
// After prompt ends, player should be null
|
||||
expect(host.activePromptPlayer.value).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue