355 lines
9.2 KiB
TypeScript
355 lines
9.2 KiB
TypeScript
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];
|
||
}
|