boardgame-core/src/games/tictactoe/TicTacToeRules.ts

355 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Rule, RuleResult, RuleContext } from '../../rules/Rule';
import { createValidationRule, createEffectRule, createTriggerRule } from '../../rules/Rule';
import type { Player, TicTacToeMetadata, MoveRecord } from './TicTacToeState';
import { getWinningCombinations, parseCellId } from './TicTacToeState';
/**
* 井字棋游戏规则集合
*/
/**
* 获取当前玩家
*/
function getCurrentPlayer(gameState: any): Player {
const metadata = gameState.data.value.metadata as Record<string, unknown> | undefined;
const ticTacToeMetadata = metadata?.ticTacToe as TicTacToeMetadata | undefined;
return ticTacToeMetadata?.currentPlayer || 'X';
}
/**
* 检查游戏是否结束
*/
function isGameEnded(gameState: any): boolean {
const metadata = gameState.data.value.metadata as Record<string, unknown> | undefined;
const ticTacToeMetadata = metadata?.ticTacToe as TicTacToeMetadata | undefined;
return ticTacToeMetadata?.gameEnded || false;
}
/**
* 更新游戏 metadata
*/
function updateGameMetadata(gameState: any, updates: Partial<TicTacToeMetadata>): void {
const metadata = gameState.data.value.metadata as Record<string, unknown> | undefined;
const currentTicTacToe = (metadata?.ticTacToe as TicTacToeMetadata) || {
currentPlayer: 'X',
gameEnded: false,
winner: null,
moveHistory: [],
totalMoves: 0,
};
gameState.data.value = {
...gameState.data.value,
metadata: {
...metadata,
ticTacToe: { ...currentTicTacToe, ...updates },
},
};
}
/**
* 获取单元格的玩家标记
*/
function getCellPlayer(
context: RuleContext,
cellId: string
): Player | null {
const placement = context.gameState.placements.value.get(cellId);
if (!placement?.metadata?.player) {
return null;
}
return placement.metadata.player as Player;
}
/**
* 检查是否有玩家获胜
*/
function checkWin(
context: RuleContext,
size: number = 3
): { winner: Player; combination: string[] } | null {
const combinations = getWinningCombinations(size);
for (const combination of combinations) {
const players = combination.map((cellId) => getCellPlayer(context, cellId));
const firstPlayer = players[0];
if (firstPlayer && players.every((p) => p === firstPlayer)) {
return { winner: firstPlayer, combination };
}
}
return null;
}
/**
* 检查是否平局(所有单元格都被填充且无获胜者)
*/
function isDraw(context: RuleContext, size: number = 3): boolean {
const totalCells = size * size;
const filledCells = Array.from(context.gameState.placements.value.values()).filter(
(p) => p.metadata?.player
).length;
return filledCells === totalCells;
}
/**
* 规则 1验证轮到当前玩家
* 在玩家尝试下子时检查是否是他们的回合
*/
export const validateTurnRule = createValidationRule({
id: 'tictactoe-validate-turn',
name: 'Validate Turn',
description: 'Check if it is the current player turn',
priority: 1,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
validate: async (context: RuleContext): Promise<RuleResult> => {
const cellId = context.command.steps[0]?.params?.cell as string;
const expectedPlayer = context.command.steps[0]?.params?.player as Player;
const currentPlayer = getCurrentPlayer(context.gameState);
if (!cellId) {
return {
success: false,
error: 'Cell ID is required',
};
}
if (expectedPlayer && expectedPlayer !== currentPlayer) {
return {
success: false,
error: `It is ${currentPlayer}'s turn, not ${expectedPlayer}'s`,
};
}
return { success: true };
},
});
/**
* 规则 2验证单元格为空
* 检查目标单元格是否已经被占用
*/
export const validateCellEmptyRule = createValidationRule({
id: 'tictactoe-validate-cell-empty',
name: 'Validate Cell Empty',
description: 'Check if the target cell is empty',
priority: 2,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
validate: async (context: RuleContext): Promise<RuleResult> => {
const cellId = context.command.steps[0]?.params?.cell as string;
if (!cellId) {
return {
success: false,
error: 'Cell ID is required',
};
}
const cellPlayer = getCellPlayer(context, cellId);
if (cellPlayer !== null) {
return {
success: false,
error: `Cell ${cellId} is already occupied by ${cellPlayer}`,
};
}
return { success: true };
},
});
/**
* 规则 3验证游戏未结束
* 游戏结束后不允许继续下子
*/
export const validateGameNotEndedRule = createValidationRule({
id: 'tictactoe-validate-game-not-ended',
name: 'Validate Game Not Ended',
description: 'Check if the game has already ended',
priority: 0,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
validate: async (context: RuleContext): Promise<RuleResult> => {
const gameEnded = isGameEnded(context.gameState);
if (gameEnded) {
return {
success: false,
error: 'Game has already ended',
blockCommand: true,
};
}
return { success: true };
},
});
/**
* 效果规则:切换玩家
* 在玩家下子后自动切换到下一个玩家
*/
export const switchTurnRule = createEffectRule({
id: 'tictactoe-switch-turn',
name: 'Switch Turn',
description: 'Switch to the next player after a move',
priority: 10,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
apply: async (context: RuleContext): Promise<RuleResult> => {
const currentPlayer = getCurrentPlayer(context.gameState);
const nextPlayer: Player = currentPlayer === 'X' ? 'O' : 'X';
// 直接更新 metadata
updateGameMetadata(context.gameState, { currentPlayer: nextPlayer });
return {
success: true,
};
},
});
/**
* 效果规则:记录移动历史
* 记录玩家的每一步移动
*/
export const recordMoveHistoryRule = createEffectRule({
id: 'tictactoe-record-history',
name: 'Record Move History',
description: 'Record the move in game history',
priority: 9,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
apply: async (context: RuleContext): Promise<RuleResult> => {
const cellId = context.command.steps[0]?.params?.cell as string;
const player = context.command.steps[0]?.params?.player as Player;
const metadata = context.gameState.data.value.metadata || {};
const ticTacToeMetadata = (metadata?.ticTacToe as TicTacToeMetadata) || {
currentPlayer: 'X',
gameEnded: false,
winner: null,
moveHistory: [],
totalMoves: 0,
};
const moveRecord: MoveRecord = {
player,
cellId,
timestamp: Date.now(),
};
const moveHistory = ticTacToeMetadata.moveHistory || [];
moveHistory.push(moveRecord);
// 直接更新 metadata
updateGameMetadata(context.gameState, {
moveHistory,
totalMoves: (ticTacToeMetadata.totalMoves || 0) + 1,
});
return {
success: true,
};
},
});
/**
* 触发规则:检查获胜条件
* 当有玩家连成一线时触发
*/
export const checkWinConditionRule = createTriggerRule({
id: 'tictactoe-check-win',
name: 'Check Win Condition',
description: 'Check if a player has won the game',
priority: 100,
gameType: 'tictactoe',
condition: async (context: RuleContext): Promise<boolean> => {
const winResult = checkWin(context);
return winResult !== null;
},
action: async (context: RuleContext): Promise<RuleResult> => {
const winResult = checkWin(context);
if (!winResult) {
return { success: false, error: 'No winner detected' };
}
const { winner, combination } = winResult;
// 直接更新 metadata
updateGameMetadata(context.gameState, {
gameEnded: true,
winner,
winningCombination: combination,
});
return {
success: true,
};
},
});
/**
* 触发规则:检查平局条件
* 当所有单元格都被填充且无获胜者时触发
*/
export const checkDrawConditionRule = createTriggerRule({
id: 'tictactoe-check-draw',
name: 'Check Draw Condition',
description: 'Check if the game is a draw',
priority: 101,
gameType: 'tictactoe',
condition: async (context: RuleContext): Promise<boolean> => {
const winResult = checkWin(context);
if (winResult !== null) {
return false; // 有获胜者,不是平局
}
return isDraw(context);
},
action: async (context: RuleContext): Promise<RuleResult> => {
// 直接更新 metadata
updateGameMetadata(context.gameState, {
gameEnded: true,
winner: null, // null 表示平局
});
return {
success: true,
};
},
});
/**
* 所有井字棋游戏规则
*/
export const ticTacToeRules: Rule[] = [
validateTurnRule,
validateCellEmptyRule,
validateGameNotEndedRule,
switchTurnRule,
recordMoveHistoryRule,
checkWinConditionRule,
checkDrawConditionRule,
];
/**
* 获取井字棋验证规则
*/
export function getTicTacToeValidationRules(): Rule[] {
return [validateTurnRule, validateCellEmptyRule, validateGameNotEndedRule];
}
/**
* 获取井字棋效果规则
*/
export function getTicTacToeEffectRules(): Rule[] {
return [switchTurnRule, recordMoveHistoryRule];
}
/**
* 获取井字棋触发规则
*/
export function getTicTacToeTriggerRules(): Rule[] {
return [checkWinConditionRule, checkDrawConditionRule];
}