361 lines
11 KiB
TypeScript
361 lines
11 KiB
TypeScript
import { Piece, Position, Color, PieceType, Move, PALACE, RIVER_ROWS } from "../types/chess";
|
||
|
||
// 获取棋盘上的棋子
|
||
export function getPieceAt(pieces: Piece[], pos: Position): Piece | undefined {
|
||
return pieces.find((p) => p.alive && p.position.row === pos.row && p.position.col === pos.col);
|
||
}
|
||
|
||
// 检查位置是否在棋盘内
|
||
export function isInBoard(pos: Position): boolean {
|
||
return pos.row >= 0 && pos.row <= 9 && pos.col >= 0 && pos.col <= 8;
|
||
}
|
||
|
||
// 检查位置是否在九宫格内
|
||
export function isInPalace(pos: Position, color: Color): boolean {
|
||
const palace = PALACE[color];
|
||
return (
|
||
pos.row >= palace.minRow &&
|
||
pos.row <= palace.maxRow &&
|
||
pos.col >= palace.minCol &&
|
||
pos.col <= palace.maxCol
|
||
);
|
||
}
|
||
|
||
// 检查是否过河
|
||
export function hasCrossedRiver(pos: Position, color: Color): boolean {
|
||
if (color === "red") {
|
||
return pos.row <= 4; // 红方向上过河
|
||
} else {
|
||
return pos.row >= 5; // 黑方向下过河
|
||
}
|
||
}
|
||
|
||
// 计算两个位置之间的棋子数(用于车和炮)
|
||
export function countPiecesBetween(pieces: Piece[], from: Position, to: Position): number {
|
||
if (from.row !== to.row && from.col !== to.col) return -1; // 不在同一直线
|
||
|
||
let count = 0;
|
||
if (from.row === to.row) {
|
||
// 横向
|
||
const minCol = Math.min(from.col, to.col);
|
||
const maxCol = Math.max(from.col, to.col);
|
||
for (let col = minCol + 1; col < maxCol; col++) {
|
||
if (getPieceAt(pieces, { row: from.row, col })) count++;
|
||
}
|
||
} else {
|
||
// 纵向
|
||
const minRow = Math.min(from.row, to.row);
|
||
const maxRow = Math.max(from.row, to.row);
|
||
for (let row = minRow + 1; row < maxRow; row++) {
|
||
if (getPieceAt(pieces, { row, col: from.col })) count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
// 检查马腿是否被挡住
|
||
export function isHorseBlocked(pieces: Piece[], from: Position, to: Position): boolean {
|
||
const rowDiff = to.row - from.row;
|
||
const colDiff = to.col - from.col;
|
||
|
||
// 马走日:必须是一个方向2格,另一个方向1格
|
||
if (Math.abs(rowDiff) === 2 && Math.abs(colDiff) === 1) {
|
||
// 纵向移动2格,检查中间点
|
||
const blockPos: Position = { row: from.row + rowDiff / 2, col: from.col };
|
||
return !!getPieceAt(pieces, blockPos);
|
||
} else if (Math.abs(rowDiff) === 1 && Math.abs(colDiff) === 2) {
|
||
// 横向移动2格,检查中间点
|
||
const blockPos: Position = { row: from.row, col: from.col + colDiff / 2 };
|
||
return !!getPieceAt(pieces, blockPos);
|
||
}
|
||
return true; // 不是日字走法,视为被挡住
|
||
}
|
||
|
||
// 检查象眼是否被挡住
|
||
export function isElephantBlocked(pieces: Piece[], from: Position, to: Position): boolean {
|
||
const blockPos: Position = {
|
||
row: (from.row + to.row) / 2,
|
||
col: (from.col + to.col) / 2,
|
||
};
|
||
return !!getPieceAt(pieces, blockPos);
|
||
}
|
||
|
||
// 验证特定棋子的走法
|
||
export function isValidPieceMove(
|
||
pieces: Piece[],
|
||
piece: Piece,
|
||
to: Position
|
||
): boolean {
|
||
const from = piece.position;
|
||
const targetPiece = getPieceAt(pieces, to);
|
||
|
||
// 不能吃自己的棋子
|
||
if (targetPiece && targetPiece.color === piece.color) return false;
|
||
|
||
const rowDiff = to.row - from.row;
|
||
const colDiff = to.col - from.col;
|
||
const absRowDiff = Math.abs(rowDiff);
|
||
const absColDiff = Math.abs(colDiff);
|
||
|
||
switch (piece.type) {
|
||
case "帅":
|
||
case "将":
|
||
// 只能在九宫格内移动
|
||
if (!isInPalace(to, piece.color)) {
|
||
// 特殊情况:将帅对面(飞将)
|
||
if (colDiff === 0) {
|
||
const target = getPieceAt(pieces, to);
|
||
if (target && (target.type === "帅" || target.type === "将")) {
|
||
return countPiecesBetween(pieces, from, to) === 0;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// 只能横竖走一格
|
||
return (absRowDiff === 1 && absColDiff === 0) || (absRowDiff === 0 && absColDiff === 1);
|
||
|
||
case "仕":
|
||
case "士":
|
||
// 只能在九宫格内移动
|
||
if (!isInPalace(to, piece.color)) return false;
|
||
// 只能斜走一格
|
||
return absRowDiff === 1 && absColDiff === 1;
|
||
|
||
case "相":
|
||
case "象":
|
||
// 不能过河
|
||
if (hasCrossedRiver(to, piece.color)) return false;
|
||
// 必须斜走2格
|
||
if (absRowDiff !== 2 || absColDiff !== 2) return false;
|
||
// 不能被象眼挡住
|
||
return !isElephantBlocked(pieces, from, to);
|
||
|
||
case "马":
|
||
// 必须走日字
|
||
if (!((absRowDiff === 2 && absColDiff === 1) || (absRowDiff === 1 && absColDiff === 2))) {
|
||
return false;
|
||
}
|
||
// 不能被马腿挡住
|
||
return !isHorseBlocked(pieces, from, to);
|
||
|
||
case "车":
|
||
// 必须横竖走
|
||
if (from.row !== to.row && from.col !== to.col) return false;
|
||
// 路径上不能有其他棋子
|
||
return countPiecesBetween(pieces, from, to) === 0;
|
||
|
||
case "炮":
|
||
// 必须横竖走
|
||
if (from.row !== to.row && from.col !== to.col) return false;
|
||
const piecesBetween = countPiecesBetween(pieces, from, to);
|
||
if (targetPiece) {
|
||
// 吃子:中间必须有且仅有1个棋子
|
||
return piecesBetween === 1;
|
||
} else {
|
||
// 移动:中间不能有棋子
|
||
return piecesBetween === 0;
|
||
}
|
||
|
||
case "兵":
|
||
case "卒":
|
||
// 只能向前走一格(不能后退)
|
||
const forwardDir = piece.color === "red" ? -1 : 1;
|
||
|
||
if (!hasCrossedRiver(from, piece.color)) {
|
||
// 未过河:只能向前走
|
||
return rowDiff === forwardDir && colDiff === 0;
|
||
} else {
|
||
// 已过河:可以向前或向左右
|
||
return (
|
||
(rowDiff === forwardDir && colDiff === 0) || // 向前
|
||
(rowDiff === 0 && absColDiff === 1) // 左右
|
||
);
|
||
}
|
||
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 检查移动后是否会被将军
|
||
export function isInCheckAfterMove(
|
||
pieces: Piece[],
|
||
from: Position,
|
||
to: Position,
|
||
color: Color
|
||
): boolean {
|
||
// 模拟移动
|
||
const newPieces = pieces.map((p) => {
|
||
if (p.position.row === from.row && p.position.col === from.col) {
|
||
return { ...p, position: to };
|
||
}
|
||
// 移除被吃的棋子
|
||
if (p.position.row === to.row && p.position.col === to.col) {
|
||
return { ...p, alive: false };
|
||
}
|
||
return p;
|
||
});
|
||
|
||
return isInCheck(newPieces, color);
|
||
}
|
||
|
||
// 检查某方是否被将军
|
||
export function isInCheck(pieces: Piece[], color: Color): boolean {
|
||
// 找到己方的帅/将
|
||
const king = pieces.find(
|
||
(p) => p.alive && p.color === color && (p.type === "帅" || p.type === "将")
|
||
);
|
||
if (!king) return false;
|
||
|
||
// 检查对方任何棋子是否能攻击到帅/将
|
||
const enemyColor = color === "red" ? "black" : "red";
|
||
const enemyPieces = pieces.filter((p) => p.alive && p.color === enemyColor);
|
||
|
||
for (const enemy of enemyPieces) {
|
||
if (isValidPieceMove(pieces, enemy, king.position)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 获取某棋子的所有合法移动
|
||
export function getValidMoves(pieces: Piece[], piece: Piece): Position[] {
|
||
const validMoves: Position[] = [];
|
||
|
||
for (let row = 0; row <= 9; row++) {
|
||
for (let col = 0; col <= 8; col++) {
|
||
const to = { row, col };
|
||
if (
|
||
isValidPieceMove(pieces, piece, to) &&
|
||
!isInCheckAfterMove(pieces, piece.position, to, piece.color)
|
||
) {
|
||
validMoves.push(to);
|
||
}
|
||
}
|
||
}
|
||
|
||
return validMoves;
|
||
}
|
||
|
||
// 检查某方是否有任何合法移动
|
||
export function hasAnyValidMove(pieces: Piece[], color: Color): boolean {
|
||
const colorPieces = pieces.filter((p) => p.alive && p.color === color);
|
||
for (const piece of colorPieces) {
|
||
if (getValidMoves(pieces, piece).length > 0) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 检查是否将死
|
||
export function isCheckmate(pieces: Piece[], color: Color): boolean {
|
||
return isInCheck(pieces, color) && !hasAnyValidMove(pieces, color);
|
||
}
|
||
|
||
// 检查是否困毙(无子可动但不是将军)
|
||
export function isStalemate(pieces: Piece[], color: Color): boolean {
|
||
return !isInCheck(pieces, color) && !hasAnyValidMove(pieces, color);
|
||
}
|
||
|
||
// 验证移动是否合法
|
||
export function isValidMove(
|
||
pieces: Piece[],
|
||
from: Position,
|
||
to: Position,
|
||
turn: Color
|
||
): { valid: boolean; reason?: string } {
|
||
const piece = getPieceAt(pieces, from);
|
||
|
||
if (!piece) return { valid: false, reason: "没有棋子" };
|
||
if (piece.color !== turn) return { valid: false, reason: "不是己方棋子" };
|
||
if (!isInBoard(to)) return { valid: false, reason: "超出棋盘" };
|
||
|
||
const targetPiece = getPieceAt(pieces, to);
|
||
if (targetPiece && targetPiece.color === piece.color) {
|
||
return { valid: false, reason: "不能吃己方棋子" };
|
||
}
|
||
|
||
if (!isValidPieceMove(pieces, piece, to)) {
|
||
return { valid: false, reason: "不符合走法规则" };
|
||
}
|
||
|
||
if (isInCheckAfterMove(pieces, from, to, turn)) {
|
||
return { valid: false, reason: "移动后会被将军" };
|
||
}
|
||
|
||
return { valid: true };
|
||
}
|
||
|
||
// 执行移动
|
||
export function makeMove(
|
||
pieces: Piece[],
|
||
from: Position,
|
||
to: Position
|
||
): { pieces: Piece[]; move: Move; capturedPiece?: Piece } {
|
||
const piece = getPieceAt(pieces, from)!;
|
||
const targetPiece = getPieceAt(pieces, to);
|
||
|
||
const newPieces = pieces.map((p) => {
|
||
if (p.id === piece.id) {
|
||
return { ...p, position: to };
|
||
}
|
||
if (targetPiece && p.id === targetPiece.id) {
|
||
return { ...p, alive: false };
|
||
}
|
||
return p;
|
||
});
|
||
|
||
// 生成走棋记录
|
||
const notation = generateNotation(piece, from, to, targetPiece);
|
||
|
||
const move: Move = {
|
||
from,
|
||
to,
|
||
piece,
|
||
capturedPiece: targetPiece,
|
||
notation,
|
||
};
|
||
|
||
return { pieces: newPieces, move, capturedPiece: targetPiece };
|
||
}
|
||
|
||
// 生成走棋记录(中文记谱法)
|
||
function generateNotation(
|
||
piece: Piece,
|
||
from: Position,
|
||
to: Position,
|
||
captured?: Piece
|
||
): string {
|
||
const colNames = ["一", "二", "三", "四", "五", "六", "七", "八", "九"];
|
||
const colNamesBlack = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
||
|
||
const fromCol = piece.color === "red" ? colNames[8 - from.col] : colNamesBlack[from.col];
|
||
const toCol = piece.color === "red" ? colNames[8 - to.col] : colNamesBlack[to.col];
|
||
|
||
const direction = piece.color === "red"
|
||
? (from.row > to.row ? "进" : from.row < to.row ? "退" : "平")
|
||
: (from.row < to.row ? "进" : from.row > to.row ? "退" : "平");
|
||
|
||
const steps = Math.abs(to.row - from.row);
|
||
const stepStr = steps > 0 ? (piece.color === "red" ? colNames[steps - 1] : String(steps)) : "";
|
||
|
||
return `${piece.type}${fromCol}${direction}${stepStr || toCol}`;
|
||
}
|
||
|
||
// 获取所有可能的移动(用于AI)
|
||
export function getAllPossibleMoves(pieces: Piece[], color: Color): { piece: Piece; to: Position }[] {
|
||
const moves: { piece: Piece; to: Position }[] = [];
|
||
const colorPieces = pieces.filter((p) => p.alive && p.color === color);
|
||
|
||
for (const piece of colorPieces) {
|
||
const validMoves = getValidMoves(pieces, piece);
|
||
for (const to of validMoves) {
|
||
moves.push({ piece, to });
|
||
}
|
||
}
|
||
|
||
return moves;
|
||
}
|