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; 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 },
@ -67,37 +68,45 @@ 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;
while (retries < maxRetries) { const playCmd = await this.prompt(
retries++; 'play <player> <row:number> <col:number> [type:string]',
const playCmd = await this.prompt('play <player> <row:number> <col:number> [type:string]'); (command) => {
const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?]; const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?];
const pieceType = type === 'cat' ? 'cat' : 'kitten'; const pieceType = type === 'cat' ? 'cat' : 'kitten';
if (player !== turnPlayer) continue; if (player !== turnPlayer) {
if (!isValidMove(row, col)) continue; return `Invalid player: ${player}. Expected ${turnPlayer}.`;
if (isCellOccupied(this.context, row, col)) continue; }
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; 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.`;
placePiece(this.context, row, col, turnPlayer, pieceType); }
applyBoops(this.context, row, col, pieceType); return null;
const graduatedLines = checkGraduation(this.context, turnPlayer);
if (graduatedLines.length > 0) {
processGraduation(this.context, turnPlayer, graduatedLines);
} }
);
const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?];
const pieceType = type === 'cat' ? 'cat' : 'kitten';
const winner = checkWinner(this.context); placePiece(this.context, row, col, turnPlayer, pieceType);
if (winner) return { winner }; 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 { function isValidMove(row: number, col: number): boolean {

View File

@ -61,28 +61,33 @@ 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;
while (retries < maxRetries) { const playCmd = await this.prompt(
retries++; 'play <player> <row:number> <col:number>',
const playCmd = await this.prompt('play <player> <row:number> <col:number>'); (command) => {
const [player, row, col] = playCmd.params as [PlayerType, number, number]; const [player, row, col] = command.params as [PlayerType, number, number];
if (player !== turnPlayer) continue; if (player !== turnPlayer) {
if (!isValidMove(row, col)) continue; return `Invalid player: ${player}. Expected ${turnPlayer}.`;
if (isCellOccupied(this.context, row, col)) continue; }
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); const winner = checkWinner(this.context);
if (winner) return { winner }; if (winner) return { winner };
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 {

View File

@ -498,14 +498,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;
// 没有验证器tryCommit 返回 null但游戏逻辑会 continue 并重新 prompt // 验证器会拒绝错误的玩家
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['black', 2, 2], options: {}, flags: {} }); 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 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} });
expect(error2).toBeNull(); expect(error2).toBeNull();
const result = await runPromise; const result = await runPromise;
@ -524,12 +522,10 @@ describe('Boop - game flow', () => {
const promptEvent1 = await promptPromise; const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 2, 2], options: {}, flags: {} }); 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 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} });
expect(error2).toBeNull(); expect(error2).toBeNull();
const result = await runPromise; const result = await runPromise;
@ -550,12 +546,10 @@ describe('Boop - game flow', () => {
const promptEvent1 = await promptPromise; const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0], options: {}, flags: {} }); 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(); promptEvent1.cancel('test end');
promptEvent2.cancel('test end');
const result = await runPromise; const result = await runPromise;
expect(result.success).toBe(false); expect(result.success).toBe(false);
@ -643,12 +637,10 @@ describe('Boop - game flow', () => {
const promptEvent1 = await promptPromise; const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['white', 0, 0, 'cat'], options: {}, flags: {} }); 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(); promptEvent1.cancel('test end');
promptEvent2.cancel('test end');
const result = await runPromise; const result = await runPromise;
expect(result.success).toBe(false); 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 runPromise = ctx.commands.run<{winner: WinnerType}>('turn X 1');
const promptEvent1 = await promptPromise; const promptEvent1 = await promptPromise;
// 没有验证器tryCommit 返回 null但游戏逻辑会 continue 并重新 prompt // 验证器会拒绝错误的玩家
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['O', 1, 1], options: {}, flags: {} }); 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 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} });
expect(error2).toBeNull(); expect(error2).toBeNull();
const result = await runPromise; const result = await runPromise;
@ -266,12 +264,10 @@ describe('TicTacToe - game flow', () => {
const promptEvent1 = await promptPromise; const promptEvent1 = await promptPromise;
const error1 = promptEvent1.tryCommit({ name: 'play', params: ['X', 1, 1], options: {}, flags: {} }); 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 = promptEvent1.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
const error2 = promptEvent2.tryCommit({ name: 'play', params: ['X', 0, 0], options: {}, flags: {} });
expect(error2).toBeNull(); expect(error2).toBeNull();
const result = await runPromise; const result = await runPromise;