Compare commits
No commits in common. "15122defcc9b10de33299d277341504402261294" and "d4d428b577b7ef061e3dcb4e4ff87ae02aebafca" have entirely different histories.
15122defcc
...
d4d428b577
|
|
@ -1656,7 +1656,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/inline-schema": {
|
"node_modules/inline-schema": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "git+https://gitea.ayi-games.online/hypercross/inline-schema#16d88d610827b5df7f77d7bc22f66b0a8a802dd7",
|
"resolved": "git+https://gitea.ayi-games.online/hypercross/inline-schema#cf55295ce79a2dcf3114e2910cc6a28ce872be90",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csv-parse": "^5.5.6"
|
"csv-parse": "^5.5.6"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ type PlayerSupply = {
|
||||||
cat: PieceSupply;
|
cat: PieceSupply;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO refactor this into an Entity
|
|
||||||
function createPlayerSupply(): PlayerSupply {
|
function createPlayerSupply(): PlayerSupply {
|
||||||
return {
|
return {
|
||||||
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
|
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
|
||||||
|
|
@ -68,33 +67,22 @@ registration.add('setup', async function() {
|
||||||
|
|
||||||
registration.add('turn <player>', async function(cmd) {
|
registration.add('turn <player>', async function(cmd) {
|
||||||
const [turnPlayer] = cmd.params as [PlayerType];
|
const [turnPlayer] = cmd.params as [PlayerType];
|
||||||
|
const maxRetries = 50;
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
const playCmd = await this.prompt(
|
while (retries < maxRetries) {
|
||||||
'play <player> <row:number> <col:number> [type:string]',
|
retries++;
|
||||||
(command) => {
|
const playCmd = await this.prompt('play <player> <row:number> <col:number> [type:string]');
|
||||||
const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?];
|
|
||||||
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
|
||||||
|
|
||||||
if (player !== turnPlayer) {
|
|
||||||
return `Invalid player: ${player}. Expected ${turnPlayer}.`;
|
|
||||||
}
|
|
||||||
if (!isValidMove(row, col)) {
|
|
||||||
return `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
|
|
||||||
}
|
|
||||||
if (isCellOccupied(this.context, row, col)) {
|
|
||||||
return `Cell (${row}, ${col}) is already occupied.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const supply = this.context.value.players[player][pieceType].supply;
|
|
||||||
if (supply <= 0) {
|
|
||||||
return `No ${pieceType}s left in ${player}'s supply.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?];
|
const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?];
|
||||||
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
const pieceType = type === 'cat' ? 'cat' : 'kitten';
|
||||||
|
|
||||||
|
if (player !== turnPlayer) continue;
|
||||||
|
if (!isValidMove(row, col)) continue;
|
||||||
|
if (isCellOccupied(this.context, row, col)) continue;
|
||||||
|
|
||||||
|
const supply = this.context.value.players[player][pieceType].supply;
|
||||||
|
if (supply <= 0) continue;
|
||||||
|
|
||||||
placePiece(this.context, row, col, turnPlayer, pieceType);
|
placePiece(this.context, row, col, turnPlayer, pieceType);
|
||||||
applyBoops(this.context, row, col, pieceType);
|
applyBoops(this.context, row, col, pieceType);
|
||||||
|
|
||||||
|
|
@ -107,6 +95,9 @@ registration.add('turn <player>', async function(cmd) {
|
||||||
if (winner) return { winner };
|
if (winner) return { winner };
|
||||||
|
|
||||||
return { winner: null };
|
return { winner: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Too many invalid attempts');
|
||||||
});
|
});
|
||||||
|
|
||||||
function isValidMove(row: number, col: number): boolean {
|
function isValidMove(row: number, col: number): boolean {
|
||||||
|
|
|
||||||
|
|
@ -61,26 +61,18 @@ registration.add('setup', async function() {
|
||||||
|
|
||||||
registration.add('turn <player> <turn:number>', async function(cmd) {
|
registration.add('turn <player> <turn:number>', async function(cmd) {
|
||||||
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
|
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
|
||||||
|
const maxRetries = MAX_TURNS * 2;
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
const playCmd = await this.prompt(
|
while (retries < maxRetries) {
|
||||||
'play <player> <row:number> <col:number>',
|
retries++;
|
||||||
(command) => {
|
const playCmd = await this.prompt('play <player> <row:number> <col:number>');
|
||||||
const [player, row, col] = command.params as [PlayerType, number, number];
|
|
||||||
|
|
||||||
if (player !== turnPlayer) {
|
|
||||||
return `Invalid player: ${player}. Expected ${turnPlayer}.`;
|
|
||||||
}
|
|
||||||
if (!isValidMove(row, col)) {
|
|
||||||
return `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
|
|
||||||
}
|
|
||||||
if (isCellOccupied(this.context, row, col)) {
|
|
||||||
return `Cell (${row}, ${col}) is already occupied.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [player, row, col] = playCmd.params as [PlayerType, number, number];
|
const [player, row, col] = playCmd.params as [PlayerType, number, number];
|
||||||
|
|
||||||
|
if (player !== turnPlayer) continue;
|
||||||
|
if (!isValidMove(row, col)) continue;
|
||||||
|
if (isCellOccupied(this.context, row, col)) continue;
|
||||||
|
|
||||||
placePiece(this.context, row, col, turnPlayer);
|
placePiece(this.context, row, col, turnPlayer);
|
||||||
|
|
||||||
const winner = checkWinner(this.context);
|
const winner = checkWinner(this.context);
|
||||||
|
|
@ -88,6 +80,9 @@ registration.add('turn <player> <turn:number>', async function(cmd) {
|
||||||
if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType };
|
if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType };
|
||||||
|
|
||||||
return { winner: null };
|
return { winner: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Too many invalid attempts');
|
||||||
});
|
});
|
||||||
|
|
||||||
function isValidMove(row: number, col: number): boolean {
|
function isValidMove(row: number, col: number): boolean {
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ export type CommandRunnerContextExport<TContext> = CommandRunnerContext<TContext
|
||||||
registry: CommandRegistry<TContext>;
|
registry: CommandRegistry<TContext>;
|
||||||
promptQueue: AsyncQueue<PromptEvent>;
|
promptQueue: AsyncQueue<PromptEvent>;
|
||||||
_activePrompt: PromptEvent | null;
|
_activePrompt: PromptEvent | null;
|
||||||
_tryCommit: (command: Command) => string | null;
|
_resolvePrompt: (command: Command) => void;
|
||||||
_cancel: (reason?: string) => void;
|
_rejectPrompt: (error: Error) => void;
|
||||||
_pendingInput: string | null;
|
_pendingInput: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,41 +66,25 @@ export function createCommandRunnerContext<TContext>(
|
||||||
|
|
||||||
let activePrompt: PromptEvent | null = null;
|
let activePrompt: PromptEvent | null = null;
|
||||||
|
|
||||||
const tryCommit = (command: Command) => {
|
const resolvePrompt = (command: Command) => {
|
||||||
if (activePrompt) {
|
if (activePrompt) {
|
||||||
const result = activePrompt.tryCommit(command);
|
activePrompt.resolve(command);
|
||||||
if (result === null) {
|
|
||||||
activePrompt = null;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return 'No active prompt';
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = (reason?: string) => {
|
|
||||||
if (activePrompt) {
|
|
||||||
activePrompt.cancel(reason);
|
|
||||||
activePrompt = null;
|
activePrompt = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = (
|
const rejectPrompt = (error: Error) => {
|
||||||
schema: CommandSchema | string,
|
if (activePrompt) {
|
||||||
validator?: (command: Command) => string | null
|
activePrompt.reject(error);
|
||||||
): Promise<Command> => {
|
activePrompt = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = (schema: Parameters<CommandRunnerContext<TContext>['prompt']>[0]): Promise<Command> => {
|
||||||
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tryCommit = (command: Command) => {
|
activePrompt = { schema: resolvedSchema, resolve, reject };
|
||||||
const error = validator?.(command);
|
const event: PromptEvent = { schema: resolvedSchema, resolve, reject };
|
||||||
if (error) return error;
|
|
||||||
resolve(command);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const cancel = (reason?: string) => {
|
|
||||||
reject(new Error(reason ?? 'Cancelled'));
|
|
||||||
};
|
|
||||||
activePrompt = { schema: resolvedSchema, tryCommit, cancel };
|
|
||||||
const event: PromptEvent = { schema: resolvedSchema, tryCommit, cancel };
|
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
listener(event);
|
listener(event);
|
||||||
}
|
}
|
||||||
|
|
@ -116,8 +100,8 @@ export function createCommandRunnerContext<TContext>(
|
||||||
on,
|
on,
|
||||||
off,
|
off,
|
||||||
_activePrompt: null,
|
_activePrompt: null,
|
||||||
_tryCommit: tryCommit,
|
_resolvePrompt: resolvePrompt,
|
||||||
_cancel: cancel,
|
_rejectPrompt: rejectPrompt,
|
||||||
_pendingInput: null,
|
_pendingInput: null,
|
||||||
promptQueue: null!
|
promptQueue: null!
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,8 @@ import type { Command, CommandSchema } from './types';
|
||||||
|
|
||||||
export type PromptEvent = {
|
export type PromptEvent = {
|
||||||
schema: CommandSchema;
|
schema: CommandSchema;
|
||||||
/**
|
resolve: (command: Command) => void;
|
||||||
* 尝试提交命令
|
reject: (error: Error) => void;
|
||||||
* @returns null - 验证成功,Promise 已 resolve
|
|
||||||
* @returns string - 验证失败,返回错误消息,Promise 未 resolve
|
|
||||||
*/
|
|
||||||
tryCommit: (command: Command) => string | null;
|
|
||||||
/** 取消 prompt,Promise 被 reject */
|
|
||||||
cancel: (reason?: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CommandRunnerEvents = {
|
export type CommandRunnerEvents = {
|
||||||
|
|
@ -28,7 +22,7 @@ export type CommandRunnerContext<TContext> = {
|
||||||
context: TContext;
|
context: TContext;
|
||||||
run: <T=unknown>(input: string) => Promise<CommandResult<T>>;
|
run: <T=unknown>(input: string) => Promise<CommandResult<T>>;
|
||||||
runParsed: (command: Command) => Promise<{ success: true; result: unknown } | { success: false; error: string }>;
|
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) => Promise<Command>;
|
||||||
on: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;
|
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;
|
off: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,7 @@ describe('createGameContext', () => {
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
expect(promptEvent.schema.name).toBe('prompt');
|
expect(promptEvent.schema.name).toBe('prompt');
|
||||||
|
|
||||||
const error = promptEvent.tryCommit({ name: 'prompt', params: ['yes'], options: {}, flags: {} });
|
promptEvent.resolve({ name: 'prompt', params: ['yes'], options: {}, flags: {} });
|
||||||
expect(error).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ describe('Boop - game flow', () => {
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
expect(promptEvent.schema.name).toBe('play');
|
expect(promptEvent.schema.name).toBe('play');
|
||||||
|
|
||||||
promptEvent.cancel('test end');
|
promptEvent.reject(new Error('test end'));
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
|
@ -480,8 +480,7 @@ describe('Boop - game flow', () => {
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
expect(promptEvent.schema.name).toBe('play');
|
expect(promptEvent.schema.name).toBe('play');
|
||||||
|
|
||||||
const error = promptEvent.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
|
promptEvent.resolve({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
|
||||||
expect(error).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -498,13 +497,12 @@ describe('Boop - game flow', () => {
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
||||||
const promptEvent1 = await promptPromise;
|
const promptEvent1 = await promptPromise;
|
||||||
// 验证器会拒绝错误的玩家
|
promptEvent1.resolve({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} });
|
||||||
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} });
|
|
||||||
expect(error1).toContain('Invalid player');
|
|
||||||
|
|
||||||
// 验证失败后,再次尝试有效输入
|
const promptEvent2 = await waitForPrompt(ctx);
|
||||||
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
|
expect(promptEvent2).not.toBeNull();
|
||||||
expect(error2).toBeNull();
|
|
||||||
|
promptEvent2.resolve({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -521,12 +519,12 @@ describe('Boop - game flow', () => {
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
||||||
const promptEvent1 = await promptPromise;
|
const promptEvent1 = await promptPromise;
|
||||||
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
|
promptEvent1.resolve({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
|
||||||
expect(error1).toContain('occupied');
|
|
||||||
|
|
||||||
// 验证失败后,再次尝试有效输入
|
const promptEvent2 = await waitForPrompt(ctx);
|
||||||
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
|
expect(promptEvent2).not.toBeNull();
|
||||||
expect(error2).toBeNull();
|
|
||||||
|
promptEvent2.resolve({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -545,11 +543,12 @@ describe('Boop - game flow', () => {
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
||||||
const promptEvent1 = await promptPromise;
|
const promptEvent1 = await promptPromise;
|
||||||
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
|
promptEvent1.resolve({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
|
||||||
expect(error1).toContain('No kittens');
|
|
||||||
|
|
||||||
// 验证失败后,取消
|
const promptEvent2 = await waitForPrompt(ctx);
|
||||||
promptEvent1.cancel('test end');
|
expect(promptEvent2).not.toBeNull();
|
||||||
|
|
||||||
|
promptEvent2.reject(new Error('test end'));
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
|
@ -562,8 +561,7 @@ describe('Boop - game flow', () => {
|
||||||
let promptPromise = waitForPrompt(ctx);
|
let promptPromise = waitForPrompt(ctx);
|
||||||
let runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
let runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
let prompt = await promptPromise;
|
let prompt = await promptPromise;
|
||||||
const error1 = prompt.tryCommit({ name: 'play', params: ['white', 3, 3], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['white', 3, 3], options: {}, flags: {} });
|
||||||
expect(error1).toBeNull();
|
|
||||||
let result = await runPromise;
|
let result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(getParts(state).length).toBe(1);
|
expect(getParts(state).length).toBe(1);
|
||||||
|
|
@ -571,8 +569,7 @@ describe('Boop - game flow', () => {
|
||||||
promptPromise = waitForPrompt(ctx);
|
promptPromise = waitForPrompt(ctx);
|
||||||
runPromise = ctx.commands.run<{winner: WinnerType}>('turn black');
|
runPromise = ctx.commands.run<{winner: WinnerType}>('turn black');
|
||||||
prompt = await promptPromise;
|
prompt = await promptPromise;
|
||||||
const error2 = prompt.tryCommit({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} });
|
||||||
expect(error2).toBeNull();
|
|
||||||
result = await runPromise;
|
result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(getParts(state).length).toBe(2);
|
expect(getParts(state).length).toBe(2);
|
||||||
|
|
@ -613,8 +610,7 @@ describe('Boop - game flow', () => {
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
||||||
const promptEvent = await promptPromise;
|
const promptEvent = await promptPromise;
|
||||||
const error = promptEvent.tryCommit({ name: 'play', params: ['white', 2, 2, 'cat'], options: {}, flags: {} });
|
promptEvent.resolve({ name: 'play', params: ['white', 2, 2, 'cat'], options: {}, flags: {} });
|
||||||
expect(error).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -636,11 +632,12 @@ describe('Boop - game flow', () => {
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
|
||||||
|
|
||||||
const promptEvent1 = await promptPromise;
|
const promptEvent1 = await promptPromise;
|
||||||
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0, 'cat'], options: {}, flags: {} });
|
promptEvent1.resolve({ name: 'play', params: ['white', 0, 0, 'cat'], options: {}, flags: {} });
|
||||||
expect(error1).toContain('No cats');
|
|
||||||
|
|
||||||
// 验证失败后,取消
|
const promptEvent2 = await waitForPrompt(ctx);
|
||||||
promptEvent1.cancel('test end');
|
expect(promptEvent2).not.toBeNull();
|
||||||
|
|
||||||
|
promptEvent2.reject(new Error('test end'));
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
expect(promptEvent.schema.name).toBe('play');
|
expect(promptEvent.schema.name).toBe('play');
|
||||||
|
|
||||||
promptEvent.cancel('test end');
|
promptEvent.reject(new Error('test end'));
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
|
@ -223,8 +223,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
expect(promptEvent.schema.name).toBe('play');
|
expect(promptEvent.schema.name).toBe('play');
|
||||||
|
|
||||||
const error = promptEvent.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
promptEvent.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
||||||
expect(error).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -240,13 +239,12 @@ describe('TicTacToe - game flow', () => {
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
|
||||||
|
|
||||||
const promptEvent1 = await promptPromise;
|
const promptEvent1 = await promptPromise;
|
||||||
// 验证器会拒绝错误的玩家
|
promptEvent1.resolve({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} });
|
||||||
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} });
|
|
||||||
expect(error1).toContain('Invalid player');
|
|
||||||
|
|
||||||
// 验证失败后,再次尝试有效输入
|
const promptEvent2 = await waitForPrompt(ctx);
|
||||||
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
expect(promptEvent2).not.toBeNull();
|
||||||
expect(error2).toBeNull();
|
|
||||||
|
promptEvent2.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -263,12 +261,12 @@ describe('TicTacToe - game flow', () => {
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
|
||||||
|
|
||||||
const promptEvent1 = await promptPromise;
|
const promptEvent1 = await promptPromise;
|
||||||
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
promptEvent1.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
||||||
expect(error1).toContain('occupied');
|
|
||||||
|
|
||||||
// 验证失败后,再次尝试有效输入
|
const promptEvent2 = await waitForPrompt(ctx);
|
||||||
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
|
expect(promptEvent2).not.toBeNull();
|
||||||
expect(error2).toBeNull();
|
|
||||||
|
promptEvent2.resolve({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -281,8 +279,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
let promptPromise = waitForPrompt(ctx);
|
let promptPromise = waitForPrompt(ctx);
|
||||||
let runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
|
let runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
|
||||||
let prompt = await promptPromise;
|
let prompt = await promptPromise;
|
||||||
const error1 = prompt.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
|
||||||
expect(error1).toBeNull();
|
|
||||||
let result = await runPromise;
|
let result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) expect(result.result.winner).toBeNull();
|
if (result.success) expect(result.result.winner).toBeNull();
|
||||||
|
|
@ -290,8 +287,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
promptPromise = waitForPrompt(ctx);
|
promptPromise = waitForPrompt(ctx);
|
||||||
runPromise = ctx.commands.run('turn O 2');
|
runPromise = ctx.commands.run('turn O 2');
|
||||||
prompt = await promptPromise;
|
prompt = await promptPromise;
|
||||||
const error2 = prompt.tryCommit({ name: 'play', params: ['O', 0, 1], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['O', 0, 1], options: {}, flags: {} });
|
||||||
expect(error2).toBeNull();
|
|
||||||
result = await runPromise;
|
result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) expect(result.result.winner).toBeNull();
|
if (result.success) expect(result.result.winner).toBeNull();
|
||||||
|
|
@ -299,8 +295,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
promptPromise = waitForPrompt(ctx);
|
promptPromise = waitForPrompt(ctx);
|
||||||
runPromise = ctx.commands.run('turn X 3');
|
runPromise = ctx.commands.run('turn X 3');
|
||||||
prompt = await promptPromise;
|
prompt = await promptPromise;
|
||||||
const error3 = prompt.tryCommit({ name: 'play', params: ['X', 1, 0], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['X', 1, 0], options: {}, flags: {} });
|
||||||
expect(error3).toBeNull();
|
|
||||||
result = await runPromise;
|
result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) expect(result.result.winner).toBeNull();
|
if (result.success) expect(result.result.winner).toBeNull();
|
||||||
|
|
@ -308,8 +303,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
promptPromise = waitForPrompt(ctx);
|
promptPromise = waitForPrompt(ctx);
|
||||||
runPromise = ctx.commands.run('turn O 4');
|
runPromise = ctx.commands.run('turn O 4');
|
||||||
prompt = await promptPromise;
|
prompt = await promptPromise;
|
||||||
const error4 = prompt.tryCommit({ name: 'play', params: ['O', 0, 2], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['O', 0, 2], options: {}, flags: {} });
|
||||||
expect(error4).toBeNull();
|
|
||||||
result = await runPromise;
|
result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) expect(result.result.winner).toBeNull();
|
if (result.success) expect(result.result.winner).toBeNull();
|
||||||
|
|
@ -317,8 +311,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
promptPromise = waitForPrompt(ctx);
|
promptPromise = waitForPrompt(ctx);
|
||||||
runPromise = ctx.commands.run('turn X 5');
|
runPromise = ctx.commands.run('turn X 5');
|
||||||
prompt = await promptPromise;
|
prompt = await promptPromise;
|
||||||
const error5 = prompt.tryCommit({ name: 'play', params: ['X', 2, 0], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['X', 2, 0], options: {}, flags: {} });
|
||||||
expect(error5).toBeNull();
|
|
||||||
result = await runPromise;
|
result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) expect(result.result.winner).toBe('X');
|
if (result.success) expect(result.result.winner).toBe('X');
|
||||||
|
|
@ -348,8 +341,7 @@ describe('TicTacToe - game flow', () => {
|
||||||
const promptPromise = waitForPrompt(ctx);
|
const promptPromise = waitForPrompt(ctx);
|
||||||
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 9');
|
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 9');
|
||||||
const prompt = await promptPromise;
|
const prompt = await promptPromise;
|
||||||
const error = prompt.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
prompt.resolve({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
|
||||||
expect(error).toBeNull();
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) expect(result.result.winner).toBe('draw');
|
if (result.success) expect(result.result.winner).toBe('draw');
|
||||||
|
|
|
||||||
|
|
@ -296,8 +296,7 @@ describe('prompt', () => {
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
|
|
||||||
const parsed = { name: 'select', params: ['Ace'], options: {}, flags: {} };
|
const parsed = { name: 'select', params: ['Ace'], options: {}, flags: {} };
|
||||||
const error = promptEvent!.tryCommit(parsed);
|
promptEvent!.resolve(parsed);
|
||||||
expect(error).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -337,7 +336,7 @@ describe('prompt', () => {
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
|
|
||||||
promptEvent!.cancel('user cancelled');
|
promptEvent!.reject(new Error('user cancelled'));
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -374,8 +373,7 @@ describe('prompt', () => {
|
||||||
expect(promptEvent).not.toBeNull();
|
expect(promptEvent).not.toBeNull();
|
||||||
expect(promptEvent!.schema.name).toBe('pick');
|
expect(promptEvent!.schema.name).toBe('pick');
|
||||||
|
|
||||||
const error = promptEvent!.tryCommit({ name: 'pick', params: ['sword'], options: {}, flags: {} });
|
promptEvent!.resolve({ name: 'pick', params: ['sword'], options: {}, flags: {} });
|
||||||
expect(error).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -412,15 +410,13 @@ describe('prompt', () => {
|
||||||
expect(promptEvents.length).toBe(1);
|
expect(promptEvents.length).toBe(1);
|
||||||
expect(promptEvents[0].schema.name).toBe('first');
|
expect(promptEvents[0].schema.name).toBe('first');
|
||||||
|
|
||||||
const error1 = promptEvents[0].tryCommit({ name: 'first', params: ['one'], options: {}, flags: {} });
|
promptEvents[0].resolve({ name: 'first', params: ['one'], options: {}, flags: {} });
|
||||||
expect(error1).toBeNull();
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
expect(promptEvents.length).toBe(2);
|
expect(promptEvents.length).toBe(2);
|
||||||
expect(promptEvents[1].schema.name).toBe('second');
|
expect(promptEvents[1].schema.name).toBe('second');
|
||||||
|
|
||||||
const error2 = promptEvents[1].tryCommit({ name: 'second', params: ['two'], options: {}, flags: {} });
|
promptEvents[1].resolve({ name: 'second', params: ['two'], options: {}, flags: {} });
|
||||||
expect(error2).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
const result = await runPromise;
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -428,93 +424,4 @@ describe('prompt', () => {
|
||||||
expect(result.result).toEqual(['one', 'two']);
|
expect(result.result).toEqual(['one', 'two']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate input with validator function', async () => {
|
|
||||||
const registry = createCommandRegistry<TestContext>();
|
|
||||||
|
|
||||||
const chooseRunner: CommandRunner<TestContext, string> = {
|
|
||||||
schema: parseCommandSchema('choose'),
|
|
||||||
run: async function () {
|
|
||||||
const result = await this.prompt(
|
|
||||||
'select <card>',
|
|
||||||
(cmd) => {
|
|
||||||
const card = cmd.params[0] as string;
|
|
||||||
if (!['Ace', 'King', 'Queen'].includes(card)) {
|
|
||||||
return `Invalid card: ${card}. Must be Ace, King, or Queen.`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return result.params[0] as string;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
registerCommand(registry, chooseRunner);
|
|
||||||
|
|
||||||
const ctx = { counter: 0, log: [] };
|
|
||||||
let promptEvent: PromptEvent | null = null;
|
|
||||||
|
|
||||||
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
|
||||||
runnerCtx.on('prompt', (e) => {
|
|
||||||
promptEvent = e;
|
|
||||||
});
|
|
||||||
|
|
||||||
const runPromise = runnerCtx.run('choose');
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
expect(promptEvent).not.toBeNull();
|
|
||||||
|
|
||||||
// Try invalid input
|
|
||||||
const invalidError = promptEvent!.tryCommit({ name: 'select', params: ['Jack'], options: {}, flags: {} });
|
|
||||||
expect(invalidError).toContain('Invalid card: Jack');
|
|
||||||
|
|
||||||
// Try valid input
|
|
||||||
const validError = promptEvent!.tryCommit({ name: 'select', params: ['Ace'], options: {}, flags: {} });
|
|
||||||
expect(validError).toBeNull();
|
|
||||||
|
|
||||||
const result = await runPromise;
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
if (result.success) {
|
|
||||||
expect(result.result).toBe('Ace');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow cancel with custom reason', async () => {
|
|
||||||
const registry = createCommandRegistry<TestContext>();
|
|
||||||
|
|
||||||
const chooseRunner: CommandRunner<TestContext, string> = {
|
|
||||||
schema: parseCommandSchema('choose'),
|
|
||||||
run: async function () {
|
|
||||||
try {
|
|
||||||
await this.prompt('select <card>');
|
|
||||||
return 'unexpected success';
|
|
||||||
} catch (e) {
|
|
||||||
return (e as Error).message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
registerCommand(registry, chooseRunner);
|
|
||||||
|
|
||||||
const ctx = { counter: 0, log: [] };
|
|
||||||
let promptEvent: PromptEvent | null = null;
|
|
||||||
|
|
||||||
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
|
||||||
runnerCtx.on('prompt', (e) => {
|
|
||||||
promptEvent = e;
|
|
||||||
});
|
|
||||||
|
|
||||||
const runPromise = runnerCtx.run('choose');
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
expect(promptEvent).not.toBeNull();
|
|
||||||
|
|
||||||
promptEvent!.cancel('custom cancellation reason');
|
|
||||||
|
|
||||||
const result = await runPromise;
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
if (result.success) {
|
|
||||||
expect(result.result).toBe('custom cancellation reason');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue