refactor: update usage pattern for tic tac toe and boop

This commit is contained in:
hyper 2026-04-02 19:32:07 +08:00
parent 5f812a3478
commit 15122defcc
4 changed files with 74 additions and 72 deletions

View File

@ -17,6 +17,7 @@ type PlayerSupply = {
cat: PieceSupply;
};
// TODO refactor this into an Entity
function createPlayerSupply(): PlayerSupply {
return {
kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 },
@ -67,21 +68,32 @@ registration.add('setup', async function() {
registration.add('turn <player>', async function(cmd) {
const [turnPlayer] = cmd.params as [PlayerType];
const maxRetries = 50;
let retries = 0;
while (retries < maxRetries) {
retries++;
const playCmd = await this.prompt('play <player> <row:number> <col:number> [type:string]');
const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?];
const playCmd = await this.prompt(
'play <player> <row:number> <col:number> [type:string]',
(command) => {
const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?];
const pieceType = type === 'cat' ? 'cat' : 'kitten';
if (player !== turnPlayer) continue;
if (!isValidMove(row, col)) continue;
if (isCellOccupied(this.context, row, col)) continue;
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) continue;
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 pieceType = type === 'cat' ? 'cat' : 'kitten';
placePiece(this.context, row, col, turnPlayer, pieceType);
applyBoops(this.context, row, col, pieceType);
@ -95,9 +107,6 @@ registration.add('turn <player>', async function(cmd) {
if (winner) return { winner };
return { winner: null };
}
throw new Error('Too many invalid attempts');
});
function isValidMove(row: number, col: number): boolean {

View File

@ -61,18 +61,26 @@ registration.add('setup', async function() {
registration.add('turn <player> <turn:number>', async function(cmd) {
const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number];
const maxRetries = MAX_TURNS * 2;
let retries = 0;
while (retries < maxRetries) {
retries++;
const playCmd = await this.prompt('play <player> <row:number> <col:number>');
const playCmd = await this.prompt(
'play <player> <row:number> <col:number>',
(command) => {
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];
if (player !== turnPlayer) continue;
if (!isValidMove(row, col)) continue;
if (isCellOccupied(this.context, row, col)) continue;
placePiece(this.context, row, col, turnPlayer);
const winner = checkWinner(this.context);
@ -80,9 +88,6 @@ registration.add('turn <player> <turn:number>', async function(cmd) {
if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType };
return { winner: null };
}
throw new Error('Too many invalid attempts');
});
function isValidMove(row: number, col: number): boolean {

View File

@ -498,14 +498,12 @@ describe('Boop - game flow', () => {
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn white');
const promptEvent1 = await promptPromise;
// 没有验证器tryCommit 返回 null但游戏逻辑会 continue 并重新 prompt
// 验证器会拒绝错误的玩家
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} });
expect(error1).toBeNull();
expect(error1).toContain('Invalid player');
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
// 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
expect(error2).toBeNull();
const result = await runPromise;
@ -524,12 +522,10 @@ describe('Boop - game flow', () => {
const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
expect(error1).toBeNull();
expect(error1).toContain('occupied');
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
// 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
expect(error2).toBeNull();
const result = await runPromise;
@ -550,12 +546,10 @@ describe('Boop - game flow', () => {
const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
expect(error1).toBeNull();
expect(error1).toContain('No kittens');
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
promptEvent2.cancel('test end');
// 验证失败后,取消
promptEvent1.cancel('test end');
const result = await runPromise;
expect(result.success).toBe(false);
@ -643,12 +637,10 @@ describe('Boop - game flow', () => {
const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0, 'cat'], options: {}, flags: {} });
expect(error1).toBeNull();
expect(error1).toContain('No cats');
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
promptEvent2.cancel('test end');
// 验证失败后,取消
promptEvent1.cancel('test end');
const result = await runPromise;
expect(result.success).toBe(false);

View File

@ -240,14 +240,12 @@ describe('TicTacToe - game flow', () => {
const runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
const promptEvent1 = await promptPromise;
// 没有验证器tryCommit 返回 null但游戏逻辑会 continue 并重新 prompt
// 验证器会拒绝错误的玩家
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} });
expect(error1).toBeNull();
expect(error1).toContain('Invalid player');
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
// 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
expect(error2).toBeNull();
const result = await runPromise;
@ -266,12 +264,10 @@ describe('TicTacToe - game flow', () => {
const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
expect(error1).toBeNull();
expect(error1).toContain('occupied');
const promptEvent2 = await waitForPrompt(ctx);
expect(promptEvent2).not.toBeNull();
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
// 验证失败后,再次尝试有效输入
const error2 = promptEvent1.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
expect(error2).toBeNull();
const result = await runPromise;