From 15122defcc9b10de33299d277341504402261294 Mon Sep 17 00:00:00 2001 From: hyper Date: Thu, 2 Apr 2026 19:32:07 +0800 Subject: [PATCH] refactor: update usage pattern for tic tac toe and boop --- src/samples/boop/index.ts | 55 ++++++++++++++++++------------- src/samples/tic-tac-toe.ts | 39 ++++++++++++---------- tests/samples/boop.test.ts | 34 ++++++++----------- tests/samples/tic-tac-toe.test.ts | 18 ++++------ 4 files changed, 74 insertions(+), 72 deletions(-) diff --git a/src/samples/boop/index.ts b/src/samples/boop/index.ts index 426d519..42bd831 100644 --- a/src/samples/boop/index.ts +++ b/src/samples/boop/index.ts @@ -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,37 +68,45 @@ registration.add('setup', async function() { registration.add('turn ', 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 [type:string]'); - const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?]; - const pieceType = type === 'cat' ? 'cat' : 'kitten'; + const playCmd = await this.prompt( + 'play [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; - - placePiece(this.context, row, col, turnPlayer, pieceType); - applyBoops(this.context, row, col, pieceType); - - const graduatedLines = checkGraduation(this.context, turnPlayer); - if (graduatedLines.length > 0) { - processGraduation(this.context, turnPlayer, graduatedLines); + 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 pieceType = type === 'cat' ? 'cat' : 'kitten'; - const winner = checkWinner(this.context); - if (winner) return { winner }; + placePiece(this.context, row, col, turnPlayer, pieceType); + applyBoops(this.context, row, col, pieceType); - return { winner: null }; + const graduatedLines = checkGraduation(this.context, turnPlayer); + if (graduatedLines.length > 0) { + processGraduation(this.context, turnPlayer, graduatedLines); } - throw new Error('Too many invalid attempts'); + const winner = checkWinner(this.context); + if (winner) return { winner }; + + return { winner: null }; }); function isValidMove(row: number, col: number): boolean { diff --git a/src/samples/tic-tac-toe.ts b/src/samples/tic-tac-toe.ts index 817eb74..de8cab1 100644 --- a/src/samples/tic-tac-toe.ts +++ b/src/samples/tic-tac-toe.ts @@ -61,28 +61,33 @@ registration.add('setup', async function() { registration.add('turn ', 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 '); - const [player, row, col] = playCmd.params as [PlayerType, number, number]; + const playCmd = await this.prompt( + 'play ', + (command) => { + const [player, row, col] = command.params as [PlayerType, number, number]; - 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.`; + } + return null; + } + ); + const [player, row, col] = playCmd.params as [PlayerType, number, number]; - placePiece(this.context, row, col, turnPlayer); + placePiece(this.context, row, col, turnPlayer); - const winner = checkWinner(this.context); - if (winner) return { winner }; - if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType }; + const winner = checkWinner(this.context); + if (winner) return { winner }; + if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType }; - return { winner: null }; - } - - throw new Error('Too many invalid attempts'); + return { winner: null }; }); function isValidMove(row: number, col: number): boolean { diff --git a/tests/samples/boop.test.ts b/tests/samples/boop.test.ts index e2b2ca7..597cacc 100644 --- a/tests/samples/boop.test.ts +++ b/tests/samples/boop.test.ts @@ -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); diff --git a/tests/samples/tic-tac-toe.test.ts b/tests/samples/tic-tac-toe.test.ts index 9083cd0..03a89a1 100644 --- a/tests/samples/tic-tac-toe.test.ts +++ b/tests/samples/tic-tac-toe.test.ts @@ -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;