feat: add currentPlayer to prompt

This commit is contained in:
2026-04-04 11:01:25 +08:00
parent d1264f3b9e
commit be4ff7ae08
6 changed files with 78 additions and 16 deletions
+17 -9
View File
@@ -1,5 +1,5 @@
import { ReadonlySignal, signal, Signal } from '@preact/signals-core';
import type { CommandSchema, CommandRegistry, CommandResult } from '@/utils/command';
import type { CommandSchema, CommandRegistry, CommandResult, PromptEvent } from '@/utils/command';
import type { MutableSignal } from '@/utils/mutable-signal';
import { createGameContext } from './game';
@@ -14,10 +14,12 @@ export class GameHost<TState extends Record<string, unknown>> {
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>>;
@@ -45,6 +47,9 @@ 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();
@@ -55,20 +60,23 @@ export class GameHost<TState extends Record<string, unknown>> {
}
private _setupPromptTracking() {
const updateSchema = () => {
const activePrompt = (this.commands as any)._activePrompt as { schema?: CommandSchema } | null;
this._activePromptSchema.value = activePrompt?.schema ?? null;
};
let currentPromptEvent: PromptEvent | null = null;
this.commands.on('prompt', () => {
updateSchema();
this.commands.on('prompt', (e) => {
currentPromptEvent = e as PromptEvent;
this._activePromptSchema.value = currentPromptEvent.schema;
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
});
this.commands.on('promptEnd', () => {
updateSchema();
currentPromptEvent = null;
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
});
updateSchema();
// Initial state
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
}
onInput(input: string): string | null {
+4 -2
View File
@@ -113,7 +113,8 @@ 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';
@@ -141,7 +142,8 @@ 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)!;
+2 -1
View File
@@ -73,7 +73,8 @@ 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];
+6 -3
View File
@@ -104,7 +104,8 @@ export function createCommandRunnerContext<TContext>(
const prompt = (
schema: CommandSchema | string,
validator?: (command: Command) => string | null
validator?: (command: Command) => string | null,
currentPlayer?: string | null
): Promise<Command> => {
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
return new Promise((resolve, reject) => {
@@ -120,10 +121,12 @@ export function createCommandRunnerContext<TContext>(
return null;
};
const cancel = (reason?: string) => {
activePrompt = null;
emitPromptEnd();
reject(new Error(reason ?? 'Cancelled'));
};
activePrompt = { schema: resolvedSchema, tryCommit, cancel };
const event: PromptEvent = { schema: resolvedSchema, tryCommit, cancel };
activePrompt = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel };
const event: PromptEvent = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel };
for (const listener of promptListeners) {
listener(event);
}
+3 -1
View File
@@ -4,6 +4,8 @@ import { applyCommandSchema } from './command-validate';
export type PromptEvent = {
schema: CommandSchema;
/** 当前等待输入的玩家 */
currentPlayer: string | null;
/**
* 尝试提交命令
* @param commandOrInput Command 对象或命令字符串
@@ -33,7 +35,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) => 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;
off: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;
};