using JoshHeaps.Net.Models; using JoshHeaps.Net.Services.Interfaces; namespace JoshHeaps.Net.Services.Implementations; public class ChessService : IChessService { public GameState CreateNewGame() { var gameState = new GameState(); InitializeBoard(gameState); return gameState; } private void InitializeBoard(GameState gameState) { for (int r = 0; r < 8; r++) for (int c = 0; c < 8; c++) gameState.Board[r, c] = null; gameState.Pieces.Clear(); gameState.MoveHistory.Clear(); gameState.SanHistory.Clear(); gameState.WhiteCanCastleKingside = true; gameState.WhiteCanCastleQueenside = true; gameState.BlackCanCastleKingside = true; gameState.BlackCanCastleQueenside = true; gameState.EnPassantTarget = null; gameState.HalfmoveClock = 0; SetupBlackPieces(gameState); SetupWhitePieces(gameState); gameState.CurrentPlayer = PieceColor.White; UpdateCheckStatus(gameState); gameState.PositionHistory.Clear(); gameState.PositionHistory.Add(PositionKey(gameState)); } private static void SetupBlackPieces(GameState gs) { var blackMajors = new[] { PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen, PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook }; for (int c = 0; c < 8; c++) { var major = new ChessPiece($"b{blackMajors[c]}{c}", blackMajors[c], PieceColor.Black, new Position(0, c)); gs.Board[0, c] = major; gs.Pieces.Add(major); } for (int c = 0; c < 8; c++) { var pawn = new ChessPiece($"bPawn{c}", PieceType.Pawn, PieceColor.Black, new Position(1, c)); gs.Board[1, c] = pawn; gs.Pieces.Add(pawn); } } private static void SetupWhitePieces(GameState gs) { var whiteMajors = new[] { PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen, PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook }; for (int c = 0; c < 8; c++) { var major = new ChessPiece($"w{whiteMajors[c]}{c}", whiteMajors[c], PieceColor.White, new Position(7, c)); gs.Board[7, c] = major; gs.Pieces.Add(major); } for (int c = 0; c < 8; c++) { var pawn = new ChessPiece($"wPawn{c}", PieceType.Pawn, PieceColor.White, new Position(6, c)); gs.Board[6, c] = pawn; gs.Pieces.Add(pawn); } } public List<(ChessPiece piece, List moves)> GetAllLegalMoves(GameState gameState) { var result = new List<(ChessPiece piece, List moves)>(); var currentPieces = gameState .Pieces .Where(p => p.Color == gameState.CurrentPlayer && p.Position.Row >= 0) .ToList(); foreach (var piece in currentPieces) { var moves = GetLegalMovesForPiece(gameState, piece.Id); if (moves.Count > 0) result.Add((piece, moves)); } return result; } public List GetLegalMovesForPiece(GameState gameState, string pieceId) { var piece = gameState.Pieces.FirstOrDefault(p => p.Id == pieceId); if (piece == null) return []; var candidateMoves = GenerateCandidateMoves(gameState, piece); var legalMoves = new List(); foreach (var pos in candidateMoves) if (IsMoveLegalConsideringCheck(gameState, piece, pos)) legalMoves.Add(pos); return legalMoves; } public MoveResultDto MakeMove(GameState gameState, MoveDto moveDto) { var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId); if (piece == null) return new MoveResultDto { Success = false, Message = "Piece not found." }; if (piece.Color != gameState.CurrentPlayer) return new MoveResultDto { Success = false, Message = "Not your turn." }; var legalMoves = GetLegalMovesForPiece(gameState, piece.Id); var targetPos = new Position(moveDto.TargetRow, moveDto.TargetCol); if (!legalMoves.Any(m => m.Row == moveDto.TargetRow && m.Col == moveDto.TargetCol)) return new MoveResultDto { Success = false, Message = "Illegal move." }; // SAN is built before the move (needs the pre-move board for captures/disambiguation); // the check/mate suffix is appended after UpdateCheckStatus. var sanBase = BuildSan(gameState, piece, targetPos, moveDto); PerformMove(gameState, piece, targetPos, moveDto); UpdateCheckStatus(gameState); var notation = $"{piece.Id}:{piece.Position}->{targetPos}"; gameState.MoveHistory.Add(notation); gameState.SanHistory.Add(sanBase + (gameState.IsCheckmate ? "#" : gameState.IsCheck ? "+" : "")); var positionKey = PositionKey(gameState); gameState.PositionHistory.Add(positionKey); if (gameState.PositionHistory.Count(k => k == positionKey) >= 3) gameState.IsThreefoldRepetition = true; return new MoveResultDto { Success = true, Message = "Move successful.", IsCheck = gameState.IsCheck, IsCheckmate = gameState.IsCheckmate, IsStalemate = gameState.IsStalemate, IsThreefoldRepetition = gameState.IsThreefoldRepetition }; } /// /// Build the standard-algebraic notation for a move from the position BEFORE it is /// applied (the check/mate suffix is added by the caller afterward). /// private string BuildSan(GameState gs, ChessPiece piece, Position target, MoveDto moveDto) { var from = piece.Position; if (piece.Type == PieceType.King && Math.Abs(target.Col - from.Col) == 2) return target.Col > from.Col ? "O-O" : "O-O-O"; bool targetOccupied = gs.Board[target.Row, target.Col] != null; bool isEnPassant = piece.Type == PieceType.Pawn && from.Col != target.Col && !targetOccupied; bool isCapture = targetOccupied || isEnPassant; string dest = SquareName(target); if (piece.Type == PieceType.Pawn) { var san = isCapture ? $"{FileChar(from.Col)}x{dest}" : dest; bool promotes = (piece.Color == PieceColor.White && target.Row == 0) || (piece.Color == PieceColor.Black && target.Row == 7); if (promotes) san += "=" + PieceLetter(moveDto.PromotionChoice ?? PieceType.Queen); return san; } return $"{PieceLetter(piece.Type)}{Disambiguation(gs, piece, target)}{(isCapture ? "x" : "")}{dest}"; } /// /// SAN disambiguation: when another piece of the same type and color can also reach the /// target, qualify the origin by file, else rank, else both. /// private string Disambiguation(GameState gs, ChessPiece piece, Position target) { var rivals = gs.Pieces .Where(p => p.Id != piece.Id && p.Type == piece.Type && p.Color == piece.Color && p.Position.Row >= 0) .Where(p => GetLegalMovesForPiece(gs, p.Id).Any(m => m.Row == target.Row && m.Col == target.Col)) .ToList(); if (rivals.Count == 0) return ""; if (rivals.All(p => p.Position.Col != piece.Position.Col)) return FileChar(piece.Position.Col).ToString(); if (rivals.All(p => p.Position.Row != piece.Position.Row)) return (8 - piece.Position.Row).ToString(); return $"{FileChar(piece.Position.Col)}{8 - piece.Position.Row}"; } private static char FileChar(int col) => (char)('a' + col); private static string SquareName(Position p) => $"{FileChar(p.Col)}{8 - p.Row}"; private static string PieceLetter(PieceType type) => type switch { PieceType.Knight => "N", PieceType.Bishop => "B", PieceType.Rook => "R", PieceType.Queen => "Q", PieceType.King => "K", _ => "" }; /// /// The repetition signature of a position: the first four FEN fields — piece placement, /// side to move, castling rights, and en-passant target. Move counters are excluded. /// private static string PositionKey(GameState gs) { var fields = gs.ToFen().Split(' '); return string.Join(' ', fields.Take(4)); } private static void PerformMove(GameState gs, ChessPiece piece, Position targetPos, MoveDto moveDto) { var oldPos = piece.Position; var captured = gs.Board[targetPos.Row, targetPos.Col]; var isPawnMove = piece.Type == PieceType.Pawn; // captured before promotion can change Type HandleEnPassantIfNeeded(gs, piece, targetPos, ref captured); gs.Board[oldPos.Row, oldPos.Col] = null; piece.Position = targetPos; gs.Board[targetPos.Row, targetPos.Col] = piece; if (captured != null && captured != piece) captured.Position = new Position(-1, -1); bool wasFirstMove = !piece.HasMoved; piece.HasMoved = true; HandleCastlingIfNeeded(gs, piece, oldPos, targetPos, wasFirstMove); HandlePawnTwoSquareMove(gs, piece, oldPos, targetPos, wasFirstMove); HandlePawnPromotionIfNeeded(piece, moveDto); UpdateCastlingRights(gs, piece, oldPos); // Reset the 50-move clock on captures and pawn moves (irreversible); else advance it. gs.HalfmoveClock = (isPawnMove || captured != null) ? 0 : gs.HalfmoveClock + 1; gs.CurrentPlayer = gs.CurrentPlayer == PieceColor.White ? PieceColor.Black : PieceColor.White; } private static void HandleEnPassantIfNeeded(GameState gs, ChessPiece piece, Position targetPos, ref ChessPiece? captured) { if (piece.Type != PieceType.Pawn || !gs.EnPassantTarget.HasValue) return; var enPassantSquare = gs.EnPassantTarget.Value; if (targetPos.Row == enPassantSquare.Row && targetPos.Col == enPassantSquare.Col) { int direction = piece.Color == PieceColor.White ? 1 : -1; var capturedPos = new Position(targetPos.Row + direction, targetPos.Col); var potentialPawn = gs.Board[capturedPos.Row, capturedPos.Col]; if (potentialPawn != null && potentialPawn.Color != piece.Color && potentialPawn.Type == PieceType.Pawn) { gs.Board[capturedPos.Row, capturedPos.Col] = null; potentialPawn.Position = new Position(-1, -1); captured = potentialPawn; } } } private static void HandleCastlingIfNeeded(GameState gs, ChessPiece piece, Position oldPos, Position targetPos, bool wasFirstMove) { if (piece.Type != PieceType.King || !wasFirstMove) return; var colDiff = targetPos.Col - oldPos.Col; if (Math.Abs(colDiff) == 2) { bool isKingside = colDiff > 0; int rookStartCol = isKingside ? 7 : 0; int rookEndCol = isKingside ? 5 : 3; var rook = gs.Board[oldPos.Row, rookStartCol]; if (rook != null && rook.Type == PieceType.Rook && !rook.HasMoved) { gs.Board[oldPos.Row, rookStartCol] = null; rook.Position = new Position(oldPos.Row, rookEndCol); gs.Board[oldPos.Row, rookEndCol] = rook; rook.HasMoved = true; } } } private static void HandlePawnTwoSquareMove(GameState gs, ChessPiece piece, Position oldPos, Position targetPos, bool wasFirstMove) { gs.EnPassantTarget = null; if (piece.Type != PieceType.Pawn || !wasFirstMove) return; var rowDiff = Math.Abs(targetPos.Row - oldPos.Row); if (rowDiff == 2) { var rowBehind = (oldPos.Row + targetPos.Row) / 2; gs.EnPassantTarget = new Position(rowBehind, oldPos.Col); } } private static void HandlePawnPromotionIfNeeded(ChessPiece piece, MoveDto moveDto) { if (piece.Type != PieceType.Pawn) return; bool promotionRow = piece.Color == PieceColor.White ? piece.Position.Row == 0 : piece.Position.Row == 7; if (!promotionRow) return; var promotionChoice = moveDto.PromotionChoice ?? PieceType.Queen; piece.Type = promotionChoice; } private static void UpdateCastlingRights(GameState gs, ChessPiece movedPiece, Position oldPos) { if (movedPiece.Type == PieceType.King) { if (movedPiece.Color == PieceColor.White) { gs.WhiteCanCastleKingside = false; gs.WhiteCanCastleQueenside = false; } else { gs.BlackCanCastleKingside = false; gs.BlackCanCastleQueenside = false; } return; } if (movedPiece.Type == PieceType.Rook) { if (movedPiece.Color == PieceColor.White) { if (oldPos.Row == 7 && oldPos.Col == 0) gs.WhiteCanCastleQueenside = false; if (oldPos.Row == 7 && oldPos.Col == 7) gs.WhiteCanCastleKingside = false; } else { if (oldPos.Row == 0 && oldPos.Col == 0) gs.BlackCanCastleQueenside = false; if (oldPos.Row == 0 && oldPos.Col == 7) gs.BlackCanCastleKingside = false; } } } private IEnumerable GenerateCandidateMoves(GameState gs, ChessPiece piece) { return piece.Type switch { PieceType.Pawn => GeneratePawnMoves(gs, piece), PieceType.Rook => GenerateRookMoves(gs, piece), PieceType.Knight => GenerateKnightMoves(gs, piece), PieceType.Bishop => GenerateBishopMoves(gs, piece), PieceType.Queen => GenerateQueenMoves(gs, piece), PieceType.King => GenerateKingMoves(gs, piece), _ => [] }; } private bool IsMoveLegalConsideringCheck(GameState gs, ChessPiece piece, Position target) { var clone = CloneGameState(gs); var clonedPiece = clone.Pieces.First(p => p.Id == piece.Id); var oldPos = clonedPiece.Position; clone.Board[oldPos.Row, oldPos.Col] = null; var captured = clone.Board[target.Row, target.Col]; if (captured != null) captured.Position = new Position(-1, -1); clonedPiece.Position = target; clone.Board[target.Row, target.Col] = clonedPiece; HandleEnPassantOnClone(clone, clonedPiece, target); HandleCastlingOnClone(clone, clonedPiece, oldPos, target); var myKing = clone .Pieces .FirstOrDefault(p => p.Color == piece.Color && p.Type == PieceType.King && p.Position.Row >= 0); if (myKing == null) return false; bool inCheck = IsSquareAttacked(clone, myKing.Position, myKing.Color); return !inCheck; } private static void HandleEnPassantOnClone(GameState clone, ChessPiece clonedPiece, Position target) { if (clonedPiece.Type != PieceType.Pawn || !clone.EnPassantTarget.HasValue) return; if (target.Row == clone.EnPassantTarget.Value.Row && target.Col == clone.EnPassantTarget.Value.Col) { int direction = clonedPiece.Color == PieceColor.White ? 1 : -1; var capturedPos = new Position(target.Row + direction, target.Col); var epCaptured = clone.Board[capturedPos.Row, capturedPos.Col]; if (epCaptured != null && epCaptured.Color != clonedPiece.Color && epCaptured.Type == PieceType.Pawn) { clone.Board[capturedPos.Row, capturedPos.Col] = null; epCaptured.Position = new Position(-1, -1); } } } private static void HandleCastlingOnClone(GameState clone, ChessPiece clonedPiece, Position oldPos, Position target) { if (clonedPiece.Type != PieceType.King || clonedPiece.HasMoved) return; int colDiff = target.Col - oldPos.Col; if (Math.Abs(colDiff) == 2) { bool isKingside = colDiff > 0; int rookStartCol = isKingside ? 7 : 0; int rookEndCol = isKingside ? 5 : 3; var rook = clone.Board[oldPos.Row, rookStartCol]; if (rook != null && rook.Type == PieceType.Rook && !rook.HasMoved) { clone.Board[oldPos.Row, rookStartCol] = null; rook.Position = new Position(oldPos.Row, rookEndCol); clone.Board[oldPos.Row, rookEndCol] = rook; } } } private bool IsSquareAttacked(GameState gs, Position square, PieceColor colorOfSquare) { var enemyColor = colorOfSquare == PieceColor.White ? PieceColor.Black : PieceColor.White; var enemyPieces = gs .Pieces .Where(p => p.Color == enemyColor && p.Position.Row >= 0) .ToList(); foreach (var enemy in enemyPieces) { var moves = GenerateCandidateMoves(gs, enemy); if (moves.Any(m => m.Row == square.Row && m.Col == square.Col)) return true; } return false; } private void UpdateCheckStatus(GameState gs) { gs.IsCheck = false; gs.IsCheckmate = false; gs.IsStalemate = false; var king = gs .Pieces .FirstOrDefault(p => p.Color == gs.CurrentPlayer && p.Type == PieceType.King && p.Position.Row >= 0); // If there's no king, that's effectively checkmate for that side. if (king == null) { gs.IsCheck = true; gs.IsCheckmate = true; return; } bool inCheck = IsSquareAttacked(gs, king.Position, gs.CurrentPlayer); gs.IsCheck = inCheck; var allMoves = GetAllLegalMoves(gs); if (allMoves.Count == 0 && inCheck) { gs.IsCheckmate = true; return; } if (allMoves.Count == 0 && !inCheck) { gs.IsStalemate = true; return; } } private static GameState CloneGameState(GameState original) { var clone = new GameState { GameId = original.GameId, CurrentPlayer = original.CurrentPlayer, EnPassantTarget = original.EnPassantTarget, WhiteCanCastleKingside = original.WhiteCanCastleKingside, WhiteCanCastleQueenside = original.WhiteCanCastleQueenside, BlackCanCastleKingside = original.BlackCanCastleKingside, BlackCanCastleQueenside = original.BlackCanCastleQueenside, MoveHistory = new List(original.MoveHistory), Board = new ChessPiece[8,8], }; foreach (var p in original.Pieces) { var copy = new ChessPiece(p.Id, p.Type, p.Color, p.Position) { HasMoved = p.HasMoved }; clone.Pieces.Add(copy); } foreach (var cp in clone.Pieces) if (cp.Position.Row >= 0) clone.Board[cp.Position.Row, cp.Position.Col] = cp; return clone; } private static List GeneratePawnMoves(GameState gs, ChessPiece piece) { var moves = new List(); int direction = piece.Color == PieceColor.White ? -1 : 1; var startRow = piece.Position.Row; var startCol = piece.Position.Col; var forward1 = startRow + direction; if (IsOnBoard(forward1, startCol) && gs.Board[forward1, startCol] == null) { moves.Add(new Position(forward1, startCol)); if (!piece.HasMoved) { var forward2 = startRow + 2 * direction; if (IsOnBoard(forward2, startCol) && gs.Board[forward2, startCol] == null) moves.Add(new Position(forward2, startCol)); } } var leftCol = startCol - 1; var rightCol = startCol + 1; if (IsOnBoard(forward1, leftCol)) { var occupant = gs.Board[forward1, leftCol]; if (occupant != null && occupant.Color != piece.Color) moves.Add(new Position(forward1, leftCol)); } if (IsOnBoard(forward1, rightCol)) { var occupant = gs.Board[forward1, rightCol]; if (occupant != null && occupant.Color != piece.Color) moves.Add(new Position(forward1, rightCol)); } if (gs.EnPassantTarget.HasValue) { var ep = gs.EnPassantTarget.Value; if (ep.Row == forward1 && Math.Abs(ep.Col - startCol) == 1) { // Verify there's an enemy pawn to capture int enemyPawnRow = piece.Color == PieceColor.White ? ep.Row + 1 : ep.Row - 1; var enemyPawn = gs.Board[enemyPawnRow, ep.Col]; if (enemyPawn != null && enemyPawn.Type == PieceType.Pawn && enemyPawn.Color != piece.Color) moves.Add(ep); } } return moves; } private static List GenerateRookMoves(GameState gs, ChessPiece piece) => GenerateSlidingMoves(gs, piece, [(1, 0), (-1, 0), (0, 1), (0, -1)]); private static List GenerateBishopMoves(GameState gs, ChessPiece piece) => GenerateSlidingMoves(gs, piece, [(1, 1), (1, -1), (-1, 1), (-1, -1)]); private static List GenerateQueenMoves(GameState gs, ChessPiece piece) => [..GenerateRookMoves(gs, piece), ..GenerateBishopMoves(gs, piece)]; private static List GenerateSlidingMoves(GameState gs, ChessPiece piece, (int dr, int dc)[] directions) { var results = new List(); var (startRow, startCol) = piece.Position; foreach (var (dr, dc) in directions) results.AddRange(GetSlidingMovesInDirection(gs, piece, startRow, startCol, dr, dc)); return results; } private static List GetSlidingMovesInDirection( GameState gs, ChessPiece piece, int row, int col, int dr, int dc) { var moves = new List(); var r = row; var c = col; while (true) { r += dr; c += dc; if (!IsOnBoard(r, c)) return moves; var occupant = gs.Board[r, c]; if (occupant == null) { moves.Add(new Position(r, c)); continue; } if (occupant.Color != piece.Color) moves.Add(new Position(r, c)); return moves; } } private static IEnumerable GenerateKnightMoves(GameState gs, ChessPiece piece) { var offsets = new (int, int)[] { (2,1), (2,-1), (-2,1), (-2,-1), (1,2), (1,-2), (-1,2), (-1,-2) }; foreach (var (dr, dc) in offsets) { var r = piece.Position.Row + dr; var c = piece.Position.Col + dc; if (!IsOnBoard(r, c)) continue; var occupant = gs.Board[r, c]; if (occupant == null || occupant.Color != piece.Color) yield return new Position(r, c); } } private List GenerateKingMoves(GameState gs, ChessPiece piece) { var results = new List(); var offsets = new[] { (1,0), (-1,0), (0,1), (0,-1), (1,1), (1,-1), (-1,1), (-1,-1) }; foreach (var (dr, dc) in offsets) { int r = piece.Position.Row + dr; int c = piece.Position.Col + dc; if (!IsOnBoard(r, c)) continue; var occupant = gs.Board[r, c]; if (occupant == null || occupant.Color != piece.Color) results.Add(new Position(r, c)); } if (!piece.HasMoved && !gs.IsCheck && piece.Color == gs.CurrentPlayer) AddCastlingMoves(gs, piece, results); return results; } private void AddCastlingMoves(GameState gs, ChessPiece king, List results) { bool hasKingsideRight = king.Color == PieceColor.White ? gs.WhiteCanCastleKingside : gs.BlackCanCastleKingside; bool hasQueensideRight = king.Color == PieceColor.White ? gs.WhiteCanCastleQueenside : gs.BlackCanCastleQueenside; var row = king.Position.Row; var col = king.Position.Col; bool areKingsideCastleSpacesEmpty = IsEmpty(row, col + 1, gs) && IsEmpty(row, col + 2, gs); bool isKingsideCastleSafe = !IsSquareAttacked(gs, new Position(row, col + 1), king.Color) && !IsSquareAttacked(gs, new Position(row, col + 2), king.Color); bool areQueensideCastleSpacesEmpty = IsEmpty(row, col - 1, gs) && IsEmpty(row, col - 2, gs) && IsEmpty(row, col - 3, gs); bool isQueensideCastleSafe = !IsSquareAttacked(gs, new Position(row, col - 1), king.Color) && !IsSquareAttacked(gs, new Position(row, col - 2), king.Color); bool canCastleKingside = areKingsideCastleSpacesEmpty && isKingsideCastleSafe && hasKingsideRight; bool canCastleQueenside = areQueensideCastleSpacesEmpty && isQueensideCastleSafe && hasQueensideRight; if (canCastleKingside) results.Add(new Position(row, col + 2)); if (canCastleQueenside) results.Add(new Position(row, col - 2)); } private static bool IsEmpty(int r, int c, GameState gs) { if (!IsOnBoard(r, c)) return false; return gs.Board[r, c] == null; } private static bool IsOnBoard(int r, int c) => r >= 0 && r < 8 && c >= 0 && c < 8; }