2025-05-31 00:44:26 +09:00
|
|
|
import { type ChessPiece, PieceType, PieceColor, type Position, type MoveResult, GameMode } from "./chess-types"
|
|
|
|
|
import { cloneBoard, findKingPosition } from "./chess-utils"
|
|
|
|
|
|
|
|
|
|
// Check if a move is valid for a specific piece
|
|
|
|
|
export function isValidMove(
|
|
|
|
|
board: (ChessPiece | null)[][],
|
|
|
|
|
from: Position,
|
|
|
|
|
to: Position,
|
|
|
|
|
currentPlayer: PieceColor,
|
|
|
|
|
gameMode: GameMode = GameMode.CLASSIC,
|
|
|
|
|
): boolean {
|
|
|
|
|
// Cannot move to the same position
|
|
|
|
|
if (from.row === to.row && from.col === to.col) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const piece = board[from.row][from.col]
|
|
|
|
|
|
|
|
|
|
// No piece at the starting position or wrong player's piece
|
|
|
|
|
if (!piece || piece.color !== currentPlayer) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetPiece = board[to.row][to.col]
|
|
|
|
|
|
|
|
|
|
// Cannot capture own piece
|
|
|
|
|
if (targetPiece && targetPiece.color === piece.color) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check piece-specific movement rules
|
|
|
|
|
let validPieceMove = false
|
|
|
|
|
|
|
|
|
|
switch (piece.type) {
|
|
|
|
|
case PieceType.PAWN:
|
|
|
|
|
validPieceMove = isValidPawnMove(board, from, to)
|
|
|
|
|
break
|
|
|
|
|
case PieceType.ROOK:
|
|
|
|
|
validPieceMove = isValidRookMove(board, from, to)
|
|
|
|
|
break
|
|
|
|
|
case PieceType.KNIGHT:
|
|
|
|
|
validPieceMove = isValidKnightMove(from, to)
|
|
|
|
|
break
|
|
|
|
|
case PieceType.BISHOP:
|
|
|
|
|
validPieceMove = isValidBishopMove(board, from, to)
|
|
|
|
|
break
|
|
|
|
|
case PieceType.QUEEN:
|
|
|
|
|
validPieceMove = isValidQueenMove(board, from, to)
|
|
|
|
|
break
|
|
|
|
|
case PieceType.KING:
|
|
|
|
|
validPieceMove = isValidKingMove(board, from, to)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!validPieceMove) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In ghost chess, forced captures take priority
|
|
|
|
|
if (gameMode === GameMode.GHOST) {
|
|
|
|
|
const captureMoves = getCaptureMoves(board, currentPlayer)
|
|
|
|
|
const isCapture = targetPiece !== null
|
|
|
|
|
|
|
|
|
|
// If there are capture moves available and this isn't a capture, it's invalid
|
|
|
|
|
if (captureMoves.length > 0 && !isCapture) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the move would put or leave the king in check (only in classic mode)
|
|
|
|
|
if (gameMode === GameMode.CLASSIC) {
|
|
|
|
|
const newBoard = cloneBoard(board)
|
|
|
|
|
newBoard[to.row][to.col] = newBoard[from.row][from.col]
|
|
|
|
|
newBoard[from.row][from.col] = null
|
|
|
|
|
|
|
|
|
|
return !isKingInCheck(newBoard, piece.color)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make a move and return the new board state
|
|
|
|
|
export function makeMove(board: (ChessPiece | null)[][], from: Position, to: Position): MoveResult {
|
|
|
|
|
const newBoard = cloneBoard(board)
|
|
|
|
|
const piece = newBoard[from.row][from.col]
|
|
|
|
|
const capturedPiece = newBoard[to.row][to.col]
|
|
|
|
|
|
|
|
|
|
if (!piece) {
|
|
|
|
|
return { newBoard, capturedPiece: null }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update hasMoved property for pawns, kings, and rooks (for castling)
|
|
|
|
|
if (piece.type === PieceType.PAWN || piece.type === PieceType.KING || piece.type === PieceType.ROOK) {
|
|
|
|
|
piece.hasMoved = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle pawn promotion
|
|
|
|
|
if (piece.type === PieceType.PAWN && (to.row === 0 || to.row === 7)) {
|
|
|
|
|
piece.type = PieceType.QUEEN // Auto-promote to queen for simplicity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle castling
|
|
|
|
|
if (piece.type === PieceType.KING && Math.abs(from.col - to.col) === 2) {
|
|
|
|
|
const isKingSide = to.col > from.col
|
|
|
|
|
const rookCol = isKingSide ? 7 : 0
|
|
|
|
|
const newRookCol = isKingSide ? from.col + 1 : from.col - 1
|
|
|
|
|
|
|
|
|
|
// Move the rook
|
|
|
|
|
newBoard[from.row][newRookCol] = newBoard[from.row][rookCol]
|
|
|
|
|
newBoard[from.row][rookCol] = null
|
|
|
|
|
|
|
|
|
|
if (newBoard[from.row][newRookCol]) {
|
2025-05-31 23:10:24 +09:00
|
|
|
newBoard[from.row][newRookCol]!.hasMoved = true
|
2025-05-31 00:44:26 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make the move
|
|
|
|
|
newBoard[to.row][to.col] = piece
|
|
|
|
|
newBoard[from.row][from.col] = null
|
|
|
|
|
|
|
|
|
|
return { newBoard, capturedPiece }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the king of the given color is in check
|
|
|
|
|
export function isCheck(board: (ChessPiece | null)[][], color: PieceColor): boolean {
|
|
|
|
|
return isKingInCheck(board, color)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the king of the given color is in checkmate
|
|
|
|
|
export function isCheckmate(board: (ChessPiece | null)[][], color: PieceColor, gameMode: GameMode = GameMode.CLASSIC): boolean {
|
|
|
|
|
// Ghost chess doesn't have checkmate
|
|
|
|
|
if (gameMode === GameMode.GHOST) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the king is not in check, it's not checkmate
|
|
|
|
|
if (!isKingInCheck(board, color)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if any move can get the king out of check
|
|
|
|
|
return !hasLegalMoves(board, color, gameMode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the position is a stalemate
|
|
|
|
|
export function isStalemate(board: (ChessPiece | null)[][], color: PieceColor, gameMode: GameMode = GameMode.CLASSIC): boolean {
|
|
|
|
|
// Ghost chess doesn't have stalemate
|
|
|
|
|
if (gameMode === GameMode.GHOST) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the king is in check, it's not stalemate
|
|
|
|
|
if (isKingInCheck(board, color)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the player has no legal moves, it's stalemate
|
|
|
|
|
return !hasLegalMoves(board, color, gameMode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to check if a player has any legal moves
|
|
|
|
|
function hasLegalMoves(board: (ChessPiece | null)[][], color: PieceColor, gameMode: GameMode = GameMode.CLASSIC): boolean {
|
|
|
|
|
for (let fromRow = 0; fromRow < 8; fromRow++) {
|
|
|
|
|
for (let fromCol = 0; fromCol < 8; fromCol++) {
|
|
|
|
|
const piece = board[fromRow][fromCol]
|
|
|
|
|
if (piece && piece.color === color) {
|
|
|
|
|
for (let toRow = 0; toRow < 8; toRow++) {
|
|
|
|
|
for (let toCol = 0; toCol < 8; toCol++) {
|
|
|
|
|
if (isValidMove(board, { row: fromRow, col: fromCol }, { row: toRow, col: toCol }, color, gameMode)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to check if the king is in check
|
|
|
|
|
function isKingInCheck(board: (ChessPiece | null)[][], kingColor: PieceColor): boolean {
|
|
|
|
|
const kingPosition = findKingPosition(board, kingColor)
|
|
|
|
|
if (!kingPosition) return false
|
|
|
|
|
|
|
|
|
|
const opponentColor = kingColor === PieceColor.WHITE ? PieceColor.BLACK : PieceColor.WHITE
|
|
|
|
|
|
|
|
|
|
// Check if any opponent piece can capture the king
|
|
|
|
|
for (let row = 0; row < 8; row++) {
|
|
|
|
|
for (let col = 0; col < 8; col++) {
|
|
|
|
|
const piece = board[row][col]
|
|
|
|
|
if (piece && piece.color === opponentColor) {
|
|
|
|
|
// Use a simplified version of isValidMove that doesn't check for check
|
|
|
|
|
// to avoid infinite recursion
|
|
|
|
|
if (canPieceMove(board, { row, col }, kingPosition)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simplified version of isValidMove that doesn't check for check
|
|
|
|
|
function canPieceMove(board: (ChessPiece | null)[][], from: Position, to: Position): boolean {
|
|
|
|
|
const piece = board[from.row][from.col]
|
|
|
|
|
if (!piece) return false
|
|
|
|
|
|
|
|
|
|
const targetPiece = board[to.row][to.col]
|
|
|
|
|
if (targetPiece && targetPiece.color === piece.color) return false
|
|
|
|
|
|
|
|
|
|
switch (piece.type) {
|
|
|
|
|
case PieceType.PAWN:
|
|
|
|
|
return isValidPawnMove(board, from, to)
|
|
|
|
|
case PieceType.ROOK:
|
|
|
|
|
return isValidRookMove(board, from, to)
|
|
|
|
|
case PieceType.KNIGHT:
|
|
|
|
|
return isValidKnightMove(from, to)
|
|
|
|
|
case PieceType.BISHOP:
|
|
|
|
|
return isValidBishopMove(board, from, to)
|
|
|
|
|
case PieceType.QUEEN:
|
|
|
|
|
return isValidQueenMove(board, from, to)
|
|
|
|
|
case PieceType.KING:
|
|
|
|
|
return isValidKingMove(board, from, to)
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a pawn move is valid
|
|
|
|
|
function isValidPawnMove(board: (ChessPiece | null)[][], from: Position, to: Position): boolean {
|
|
|
|
|
const piece = board[from.row][from.col]
|
|
|
|
|
if (!piece || piece.type !== PieceType.PAWN) return false
|
|
|
|
|
|
|
|
|
|
const direction = piece.color === PieceColor.WHITE ? -1 : 1
|
|
|
|
|
const startRow = piece.color === PieceColor.WHITE ? 6 : 1
|
|
|
|
|
|
|
|
|
|
// Moving forward one square
|
|
|
|
|
if (from.col === to.col && from.row + direction === to.row && !board[to.row][to.col]) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Moving forward two squares from starting position
|
|
|
|
|
if (
|
|
|
|
|
from.col === to.col &&
|
|
|
|
|
from.row === startRow &&
|
|
|
|
|
from.row + 2 * direction === to.row &&
|
|
|
|
|
!board[from.row + direction][from.col] &&
|
|
|
|
|
!board[to.row][to.col]
|
|
|
|
|
) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Capturing diagonally
|
|
|
|
|
if ((from.col + 1 === to.col || from.col - 1 === to.col) && from.row + direction === to.row) {
|
|
|
|
|
return board[to.row][to.col] !== null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a rook move is valid
|
|
|
|
|
function isValidRookMove(board: (ChessPiece | null)[][], from: Position, to: Position): boolean {
|
|
|
|
|
// Rooks move horizontally or vertically
|
|
|
|
|
if (from.row !== to.row && from.col !== to.col) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the path is clear
|
|
|
|
|
if (from.row === to.row) {
|
|
|
|
|
// Horizontal movement
|
|
|
|
|
const start = Math.min(from.col, to.col)
|
|
|
|
|
const end = Math.max(from.col, to.col)
|
|
|
|
|
|
|
|
|
|
for (let col = start + 1; col < end; col++) {
|
|
|
|
|
if (board[from.row][col] !== null) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Vertical movement
|
|
|
|
|
const start = Math.min(from.row, to.row)
|
|
|
|
|
const end = Math.max(from.row, to.row)
|
|
|
|
|
|
|
|
|
|
for (let row = start + 1; row < end; row++) {
|
|
|
|
|
if (board[row][from.col] !== null) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a knight move is valid
|
|
|
|
|
function isValidKnightMove(from: Position, to: Position): boolean {
|
|
|
|
|
const rowDiff = Math.abs(from.row - to.row)
|
|
|
|
|
const colDiff = Math.abs(from.col - to.col)
|
|
|
|
|
|
|
|
|
|
// Knights move in an L-shape: 2 squares in one direction and 1 square perpendicular
|
|
|
|
|
return (rowDiff === 2 && colDiff === 1) || (rowDiff === 1 && colDiff === 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a bishop move is valid
|
|
|
|
|
function isValidBishopMove(board: (ChessPiece | null)[][], from: Position, to: Position): boolean {
|
|
|
|
|
const rowDiff = Math.abs(from.row - to.row)
|
|
|
|
|
const colDiff = Math.abs(from.col - to.col)
|
|
|
|
|
|
|
|
|
|
// Bishops move diagonally
|
|
|
|
|
if (rowDiff !== colDiff) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the path is clear
|
|
|
|
|
const rowDirection = from.row < to.row ? 1 : -1
|
|
|
|
|
const colDirection = from.col < to.col ? 1 : -1
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < rowDiff; i++) {
|
|
|
|
|
if (board[from.row + i * rowDirection][from.col + i * colDirection] !== null) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a queen move is valid
|
|
|
|
|
function isValidQueenMove(board: (ChessPiece | null)[][], from: Position, to: Position): boolean {
|
|
|
|
|
// Queens can move like rooks or bishops
|
|
|
|
|
return isValidRookMove(board, from, to) || isValidBishopMove(board, from, to)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a king move is valid
|
|
|
|
|
function isValidKingMove(board: (ChessPiece | null)[][], from: Position, to: Position): boolean {
|
|
|
|
|
const rowDiff = Math.abs(from.row - to.row)
|
|
|
|
|
const colDiff = Math.abs(from.col - to.col)
|
|
|
|
|
|
|
|
|
|
// Kings move one square in any direction
|
|
|
|
|
if (rowDiff <= 1 && colDiff <= 1) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for castling
|
|
|
|
|
const piece = board[from.row][from.col]
|
|
|
|
|
if (piece && piece.type === PieceType.KING && !piece.hasMoved && rowDiff === 0 && colDiff === 2) {
|
|
|
|
|
// Determine if it's kingside or queenside castling
|
|
|
|
|
const isKingSide = to.col > from.col
|
|
|
|
|
const rookCol = isKingSide ? 7 : 0
|
|
|
|
|
|
|
|
|
|
// Check if the rook is in place and hasn't moved
|
|
|
|
|
const rook = board[from.row][rookCol]
|
|
|
|
|
if (!rook || rook.type !== PieceType.ROOK || rook.color !== piece.color || rook.hasMoved) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the path is clear
|
|
|
|
|
const start = isKingSide ? from.col + 1 : rookCol + 1
|
|
|
|
|
const end = isKingSide ? rookCol : from.col
|
|
|
|
|
|
|
|
|
|
for (let col = start; col < end; col++) {
|
|
|
|
|
if (board[from.row][col] !== null) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the king is in check or would pass through check
|
|
|
|
|
const tempBoard = cloneBoard(board)
|
|
|
|
|
tempBoard[from.row][from.col] = null
|
|
|
|
|
|
|
|
|
|
// Check if the king is in check
|
|
|
|
|
if (isKingInCheck(tempBoard, piece.color)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the king would pass through check
|
|
|
|
|
tempBoard[from.row][isKingSide ? from.col + 1 : from.col - 1] = piece
|
|
|
|
|
if (isKingInCheck(tempBoard, piece.color)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all possible capture moves for a player (for ghost chess)
|
|
|
|
|
export function getCaptureMoves(board: (ChessPiece | null)[][], color: PieceColor): { from: Position; to: Position }[] {
|
|
|
|
|
const captureMoves: { from: Position; to: Position }[] = []
|
|
|
|
|
|
|
|
|
|
for (let fromRow = 0; fromRow < 8; fromRow++) {
|
|
|
|
|
for (let fromCol = 0; fromCol < 8; fromCol++) {
|
|
|
|
|
const piece = board[fromRow][fromCol]
|
|
|
|
|
if (piece && piece.color === color) {
|
|
|
|
|
for (let toRow = 0; toRow < 8; toRow++) {
|
|
|
|
|
for (let toCol = 0; toCol < 8; toCol++) {
|
|
|
|
|
const targetPiece = board[toRow][toCol]
|
|
|
|
|
if (targetPiece && targetPiece.color !== color) {
|
|
|
|
|
// Check if this piece can capture the target
|
|
|
|
|
if (canPieceMove(board, { row: fromRow, col: fromCol }, { row: toRow, col: toCol })) {
|
|
|
|
|
captureMoves.push({
|
|
|
|
|
from: { row: fromRow, col: fromCol },
|
|
|
|
|
to: { row: toRow, col: toCol }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return captureMoves
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a player has lost all their pieces (ghost chess win condition)
|
|
|
|
|
export function hasLostAllPieces(board: (ChessPiece | null)[][], color: PieceColor): boolean {
|
|
|
|
|
for (let row = 0; row < 8; row++) {
|
|
|
|
|
for (let col = 0; col < 8; col++) {
|
|
|
|
|
const piece = board[row][col]
|
|
|
|
|
if (piece && piece.color === color) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|