/// <summary> /// Checks if a square is either empty, or has an opponent piece in it /// It also performs bounds checking /// </summary> /// <param name="col">int based col (can be out of bounds)</param> /// <param name="row">int based row (can be out of bounds)</param> /// <param name="board">ChessBoard to check</param> /// <param name="playerColor">PieceColor of the player</param> /// <param name="occupied">set to true if an opponent piece is also there</param> /// <returns>true if the square is empty or contains an opponent piece</returns> public static bool SquareIsFreeOrContainsOpponent(int col, int row, ChessBoard board, PieceColor playerColor, out bool occupied) { bool result = false; occupied = false; if (BoardSquare.IsValid(col, row)) { // Get the piece at the square if any ChessPiece tPiece = board.FindPieceAt(new PieceFile(col), row); // No piece... if (tPiece == null) { result = true; occupied = false; } else // ...or opponent piece { PieceColor opponentColor = (playerColor == PieceColor.White) ? PieceColor.Black : PieceColor.White; if (tPiece.Color == opponentColor) { result = true; occupied = true; } } } return(result); }
/// <summary> /// Create a new MoveInformation struct and fill in the minumum /// required information /// </summary> /// <param name="start">starting square</param> /// <param name="end">ending square</param> /// <param name="deployed">true if piece has ever moved, prior to this move</param> /// <param name="oldFEN">last FEN for board</param> public MoveInformation(BoardSquare start, BoardSquare end, bool deployed, string oldFEN) { prevFEN = oldFEN; ancillaryPiece = null; promotionClass = PieceClass.King; // Invalid promotion job startSquare = start; endSquare = end; firstMove = (deployed == false); color = PieceColor.White; // assume white castlingRights = BoardSide.None; isCapture = false; isCastle = false; }
/// <summary> /// Process the input when waiting for the current player to select a move /// </summary> /// <param name="x">x coordinate in form</param> /// <param name="y">y coordinate in form</param> private void OnWaitingForMoveSelection(int x, int y) { BoardSquare square = ((IChessBoardView)view).GetSquare(x, y); foreach (BoardSquare move in legalMoves) { if (move == square) { // Done - this is the move MoveInformation moveInfo = new MoveInformation( new BoardSquare(selectedPiece.File, selectedPiece.Rank), move, selectedPiece.Deployed, board.CurrentFEN); moveInfo.Color = selectedPiece.Color; moveInfo.CastlingRights = board.ActivePlayerCastlingRights; Debug.WriteLine("Valid Move Detected: [{0},{1}]=>[{2},{3}]", selectedPiece.File, selectedPiece.Rank, move.File, move.Rank); // Need to detect promotion and launch dialog for it... bool isPawnMovingToBackRank = (selectedPiece.Color == PieceColor.White) ? (moveInfo.End.Rank == 8) : (moveInfo.End.Rank == 1); if ((selectedPiece.Job == PieceClass.Pawn) && isPawnMovingToBackRank) { PieceClass promotionJob = view.ChoosePromotionJob(); board.PromotePiece(moveInfo.Start.File, moveInfo.Start.Rank, moveInfo.End.File, moveInfo.End.Rank, promotionJob, ref moveInfo); } // Always returns true now board.MovePiece(ref moveInfo); view.Invalidate(); Debug.WriteLine(String.Format("Fullmoves: {0}", board.FullMoveCount)); Debug.WriteLine(String.Format("Halfmoves: {0}", board.HalfMoveCount)); Debug.WriteLine(String.Format("WhCastle: {0}", board.WhiteCastlingRights.ToString())); Debug.WriteLine(String.Format("BlCastle: {0}", board.BlackCastlingRights.ToString())); // Update the position with the engine UpdateEnginePosition(); break; } } // Either way this gets cleared legalMoves.Clear(); ((IChessBoardView)view).ClearHiglightedSquares(); selectedPiece = null; }
/// <summary> /// Extracts just the enpassant target if any from the FEN /// </summary> /// <param name="fen">FEN string to parse</param> /// <param name="enPassantSquare">BoardSquare with the target if return is true</param> /// <returns>If true, enPassantSquare holds the target, otherwise it will be a1(never valid)</returns> public static bool ExtractEnPassantTarget(string fen, out BoardSquare enPassantSquare) { string[] fenTokens = TokenizeFEN(fen); bool result = false; string enpassant = fenTokens[3]; enPassantSquare = new BoardSquare(new PieceFile('a'), 1); // '-' or a square like e5 if (String.Compare(enpassant, "-") != 0) { result = true; enPassantSquare = new BoardSquare(new PieceFile(enpassant[0]), Convert.ToInt16(enpassant[1]) - Convert.ToUInt16('0')); } return(result); }
/// <summary> /// Returns the current en-passant target square (if any) /// </summary> /// <param name="target">BoardSquare that is the en-passant target</param> /// <returns>true if target exists, in which case 'target' contains the square. /// false if there is no target, and the contents of 'target' are invalid</returns> public bool GetEnPassantTarget(out BoardSquare target) { target = enPassantTarget; return(enPassantValid); }
/// <summary> /// Produces a new FEN given a FEN and a move string /// </summary> /// <param name="fen">Starting FEN</param> /// <param name="sanMove">move e.g. e2e4 or d7c8q</param> /// <returns>Updated FEN for new position</returns> public static string ApplyMoveToFEN(string fen, string sanMove) { // Extract the original pieces of the FEN string[] fenTokens = TokenizeFEN(fen); // Extract start and end squares and promotion info BoardSquare startSquare = new BoardSquare( new PieceFile(sanMove[0]), (Convert.ToInt16(sanMove[1]) - Convert.ToInt16('0'))); BoardSquare endSquare = new BoardSquare( new PieceFile(sanMove[2]), (Convert.ToInt16(sanMove[3]) - Convert.ToInt16('0'))); bool isPromotion = (sanMove.Length == 5); PieceColor activePlayer = ExtractActivePlayer(fen); // token[2] is castling rights BoardSide whiteCastlingRights = BoardSide.None; BoardSide blackCastlingRights = BoardSide.None; FenParser.ParseCastlingRights(fenTokens[2], ref whiteCastlingRights, ref blackCastlingRights); // Get the current move counts int halfMoves = 0; int fullMoves = 0; ExtractMoveCounts(fen, ref halfMoves, ref fullMoves); // Needed for move count updates bool isCapture = false; bool isPawnMove = false; bool isNewEnPassantNeeded = false; // Expand the start and target ranks (one or both if different) fen = ExpandRank(fen, startSquare.Rank); if (startSquare.Rank != endSquare.Rank) { fen = ExpandRank(fen, endSquare.Rank); } // Get the piece moving char fenPiece = PieceAtBoardPosition(fen, startSquare.File.ToInt(), startSquare.Rank); PieceColor pieceColor = char.IsUpper(fenPiece) ? PieceColor.White : PieceColor.Black; if (isPromotion) { isPawnMove = true; fenPiece = (activePlayer == PieceColor.White) ? char.ToUpper(sanMove[4]) : sanMove[4];// Update the piece type } // target piece if any char fenTargetPiece = PieceAtBoardPosition(fen, endSquare.File.ToInt(), endSquare.Rank); // Common lambda for castling updates - used on rook moves and captures UpdateCastlingRightsOnEqualRank UpdateCastlingRightsIfNeeded = (rankA, rankB, targetSquare, color) => { if (rankA == rankB) { // A File? if (targetSquare.File.ToInt() == 1) { if (color == PieceColor.White) { whiteCastlingRights &= ~BoardSide.Queen; } else { blackCastlingRights &= ~BoardSide.Queen; } } // H File? else if (targetSquare.File.ToInt() == 8) { if (color == PieceColor.White) { whiteCastlingRights &= ~BoardSide.King; } else { blackCastlingRights &= ~BoardSide.King; } } } }; // generic capture // en-passant caught under pawn moves if (fenTargetPiece != '1') { isCapture = true; // If captured piece is enemy ROOK, that will potentially remove // rights on the opponent's side (if at home) - you can't capture // the king, so no need to check there PieceColor opponentColor = ChessBoard.OppositeColor(activePlayer); UpdateCastlingRightsIfNeeded(ChessBoard.RookHomeRank(opponentColor), endSquare.Rank, endSquare, opponentColor); } // Check the type of piece moving if (Char.ToUpper(fenPiece) == 'P') // PAWN { isPawnMove = true; // resets halfmoves // en-passant only matters in pawn moves if (String.Compare(fenTokens[3], "-") != 0) { // There is an en-passant square BoardSquare enPassantSquare = new BoardSquare( new PieceFile(fenTokens[3][0]), (Convert.ToInt16(fenTokens[3][1]) - Convert.ToInt16('0'))); // If the en-passant target is the move target, this is a capture if (enPassantSquare == endSquare) { // en-passant capture - must also expand the rank 'behind' the target and // remove that pawn - mark as capture int captureRank = (pieceColor == PieceColor.White) ? endSquare.Rank - 1 : endSquare.Rank + 1; char captured; fen = FenParser.RemovePiece(fen, endSquare.File.ToInt(), captureRank, out captured); if (captured == '1') { throw new InvalidOperationException(); } isCapture = true; } } else if (Math.Abs(endSquare.Rank - startSquare.Rank) == 2) { // If there is an enemy pawn on either side of the endSquare, // then we need to create an enpassant target // rank is already expanded char neighbor = '1'; if (endSquare.File.ToInt() > 1) { neighbor = PieceAtBoardPosition(fen, endSquare.File.ToInt() - 1, endSquare.Rank); if (char.ToUpper(neighbor) == 'P' && (ColorFromFen(neighbor) != activePlayer)) { isNewEnPassantNeeded = true; } } if (endSquare.File.ToInt() < 8) { neighbor = PieceAtBoardPosition(fen, endSquare.File.ToInt() + 1, endSquare.Rank); if (char.ToUpper(neighbor) == 'P' && (ColorFromFen(neighbor) != activePlayer)) { isNewEnPassantNeeded = true; } } } } if (Char.ToUpper(fenPiece) == 'K') // KING { // Check if this is a castling move - only time king moves 2 squares if (Math.Abs(endSquare.File.ToInt() - startSquare.File.ToInt()) == 2) { // Move the associated rook... already expanded the rank int rookFileStart = endSquare.File.ToInt() == 7 ? 8 : 1; int rookFileEnd = rookFileStart == 1 ? 4 : 6; char rook; fen = FenParser.RemovePiece(fen, rookFileStart, endSquare.Rank, out rook); fen = FenParser.InsertPiece(fen, rook, rookFileEnd, endSquare.Rank); } // Moving the king removes all rights on all sides if (activePlayer == PieceColor.White) { whiteCastlingRights = BoardSide.None; } else { blackCastlingRights = BoardSide.None; } } if (Char.ToUpper(fenPiece) == 'R') // ROOK { // Check if at home position at start int homeRank = (pieceColor == PieceColor.White) ? 1 : 8; UpdateCastlingRightsIfNeeded(homeRank, startSquare.Rank, startSquare, activePlayer); } // Remove piece char fenChar; fen = RemovePiece(fen, startSquare.File.ToInt(), startSquare.Rank, out fenChar); if ((fenPiece != fenChar) && !isPromotion) { throw new InvalidOperationException(); } // Place piece (it might not be the same type (promotions) - TODO fen = InsertPiece(fen, fenPiece, endSquare.File.ToInt(), endSquare.Rank); // Collapse the rows we touched fen = CollapseRank(fen, startSquare.Rank); if (startSquare.Rank != endSquare.Rank) { fen = CollapseRank(fen, endSquare.Rank); } // Re-tokenize fenTokens = TokenizeFEN(fen); // castling string wcr = TokenizeCastlingRights(whiteCastlingRights, PieceColor.White); string bcr = TokenizeCastlingRights(blackCastlingRights, PieceColor.Black); if ((whiteCastlingRights == BoardSide.None) && (blackCastlingRights == BoardSide.None)) { fenTokens[2] = "-"; } else { fenTokens[2] = String.Empty; if (whiteCastlingRights != BoardSide.None) { fenTokens[2] = wcr; } if (blackCastlingRights != BoardSide.None) { fenTokens[2] = String.Concat(fenTokens[2], bcr); } } // Update the other pieces of the FEN // active player fenTokens[1] = (activePlayer == PieceColor.White) ? "b" : "w"; // Did we create a new en-passant target? if (isNewEnPassantNeeded) { // Target is behind pawn int enpassantTargetRank = (activePlayer == PieceColor.White) ? endSquare.Rank - 1 : endSquare.Rank + 1; BoardSquare ept = new BoardSquare(endSquare.File, enpassantTargetRank); fenTokens[3] = ept.ToString(); } else { fenTokens[3] = "-"; } // half moves halfMoves++; if (isCapture || isPawnMove) { halfMoves = 0; } fenTokens[4] = halfMoves.ToString(); // full moves if (activePlayer == PieceColor.Black) { fullMoves++; } fenTokens[5] = fullMoves.ToString(); fen = string.Join(" ", fenTokens); return(fen); }
/// <summary> /// Returns a list of valid squares a king can move to. The list can be empty /// </summary> /// <param name="piece">ChessPiece to examine</param> /// <param name="board">ChessBoard the piece exists within</param> /// <returns>List of valid squares, or empty list</returns> public static List <BoardSquare> GetLegalMoves_King(ChessPiece piece, ChessBoard board) { List <BoardSquare> moves = new List <BoardSquare>(); /* The King is special. He can move one square in any direction * (provided it is on the board) so long as the square is empty or * it has an opponent piece on it. However, the King can never * move into check, even if the square or capture would be legal * otherwise, so it requires some extra checking. * * Further complicating things, if the king is castling, he cannot * move THROUGH check either (basically check those squares as if * they were final destinations) * * +---+---+---+---+---+---+---+---+ * | | | | | | | | | * +---+---+---+---+---+---+---+---+ r = enemy rook (block) * | | | | | | p | | | T = Move or capture * +---+---+---+---+---+---+---+---+ p = enemy pawn (block) * | | | T | T | X | | b | | b = enemy bishop (block) * +---+---+---+---+---+---+---+---+ X = illegal move * | | | T | K | T | | | | * +---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+ * | | | T | T | X | | | | | K | T | | | | | * +---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+ * | | | | | | | | | | X | X | | | | r | * +---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+ * | | | | | | | | | | | | | | | | * +---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+ */ // Cannot castle if in check if (!IsSquareInCheck(board, piece.File, piece.Rank, piece.Color)) { /* Castling may only be done if the king has never moved, the rook involved has never moved, * the squares between the king and the rook involved are unoccupied, the king is not in check, * and the king does not cross over or end on a square in which it would be in check. * * The ChessBoard will keep track of the castling rights when various pieces move, but it * won't constantly update the legality of the move */ // Build a list of squares to check BoardSide castlingRights = (piece.Color == PieceColor.White) ? board.WhiteCastlingRights : board.BlackCastlingRights; BoardSide[] sidesToCheck = new BoardSide[2] { BoardSide.King, BoardSide.Queen }; foreach (BoardSide sideToCheck in sidesToCheck) { // Backrank depends on color int kingRank = (piece.Color == PieceColor.White) ? 1 : 8; BoardSquare[] squares = null; // First check if we still have the right, if not, no need to persue it if (castlingRights.HasFlag(sideToCheck)) { squares = new BoardSquare[2]; // The target Files depend on the side of the board we're checking // put the final target in [0] if (sideToCheck == BoardSide.King) { squares[0] = new BoardSquare(new PieceFile(7), kingRank); squares[1] = new BoardSquare(new PieceFile(6), kingRank); } else // Queenside { squares[0] = new BoardSquare(new PieceFile(3), kingRank); squares[1] = new BoardSquare(new PieceFile(4), kingRank); } } // There should be 2 and only 2 from above if we found potential targets if (squares != null) { // must be empty and not in check - empty is faster so verify it first if ((board.FindPieceAt(squares[0].File, squares[0].Rank) == null) && (board.FindPieceAt(squares[1].File, squares[1].Rank) == null)) { // Now make sure neither square is in check if (!IsSquareInCheck(board, squares[0].File, squares[0].Rank, piece.Color) && !IsSquareInCheck(board, squares[1].File, squares[1].Rank, piece.Color)) { // King can still castle to this side, add the move option moves.Add(squares[0]); } } } } } // Check each of the 8 squares around the king. If it's free or has // an enemy piece, then check if it's targetable by the opponent // (moving into check) If not, then add it to the list CheckPieceTargets checkKingTargets = (p, fileDelta, rankDelta, b, m) => { // Verify targets are reachable (not off edge) int startCol = p.File.ToInt(); int startRow = p.Rank; int endCol = startCol + (fileDelta); int endRow = startRow + (rankDelta); bool occupied; // ignored here if (SquareIsFreeOrContainsOpponent(endCol, endRow, b, p.Color, out occupied)) { m.Add(new BoardSquare(new PieceFile(endCol), endRow)); } }; // Check all 8 squares around the king checkKingTargets(piece, 0, 1, board, moves); checkKingTargets(piece, 0, -1, board, moves); checkKingTargets(piece, 1, 0, board, moves); checkKingTargets(piece, -1, 0, board, moves); checkKingTargets(piece, 1, 1, board, moves); checkKingTargets(piece, -1, -1, board, moves); checkKingTargets(piece, 1, -1, board, moves); checkKingTargets(piece, -1, 1, board, moves); // Check violations are handled by the common caller for regulatr moves return(moves); }
/// <summary> /// Checks if a given piece can target a specified square. It is not required /// that the square be occupied by an enemy piece, just potentially reachable /// </summary> /// <param name="board">ChessBoard to check against</param> /// <param name="piece">ChessPiece to check</param> /// <param name="targetFile">file to target</param> /// <param name="targetRank">rank to target</param> /// <returns>true if the piece can target the given square</returns> public static bool CanPieceTargetSquare(ChessBoard board, ChessPiece piece, PieceFile targetFile, int targetRank) { bool result = false; if (piece.Captured == true) { return(false); } BoardSquare targetSquare = new BoardSquare(targetFile, targetRank); List <BoardSquare> moves = new List <BoardSquare>(); switch (piece.Job) { case PieceClass.Pawn: // Can't reuse the normal helper as it requires the space to be occupied // also w/o en-passant and double moves, this can be simpler int pawnTargetRank = (piece.Color == PieceColor.White) ? piece.Rank + 1 : piece.Rank - 1; if (targetRank == pawnTargetRank) { if (((piece.File.ToInt() - 1) == targetFile.ToInt()) || ((piece.File.ToInt() + 1) == targetFile.ToInt())) { result = true; } } break; case PieceClass.Knight: moves = GetLegalMoves_Knight(piece, board); break; case PieceClass.Bishop: moves = GetLegalMoves_Bishop(piece, board); break; case PieceClass.Rook: moves = GetLegalMoves_Rook(piece, board); break; case PieceClass.Queen: moves = GetLegalMoves_Queen(piece, board); break; case PieceClass.King: // don't recurse into the normal call, also alternate method to examine // These are pairs of offsets (-1, 0), (-1, 1),...etc so there are twice // as many of these as squares to check int[] offsets = new int[] { -1, 0, -1, 1, -1, -1, 1, 0, 1, 1, 1, -1, 0, 1, 0, -1 }; for (int index = 0; index < offsets.Length / 2; index++) { int fileOffset = offsets[index * 2]; int rankOffset = offsets[(index * 2) + 1]; // Test the validity of the square offset if (BoardSquare.IsValid(piece.File.ToInt() + fileOffset, piece.Rank + rankOffset)) { BoardSquare testSquare = new BoardSquare(new PieceFile(piece.File.ToInt() + fileOffset), piece.Rank + rankOffset); if (testSquare == targetSquare) { result = true; break; } } } break; default: throw new ArgumentOutOfRangeException(); } // King is special above if (piece.Job != PieceClass.King) { // Check moves for the target square foreach (BoardSquare square in moves) { if (square == targetSquare) { result = true; break; } } } return(result); }
/// <summary> /// Returns a list of valid squares a pawn can move to. The list can be empty /// </summary> /// <param name="piece">ChessPiece to examine</param> /// <param name="board">ChessBoard the piece exists within</param> /// <returns>List of valid squares, or empty list</returns> public static List <BoardSquare> GetLegalMoves_Pawn(ChessPiece piece, ChessBoard board) { List <BoardSquare> moves = new List <BoardSquare>(); /* Pawns can move one space forward on any move provided the square is empty * and 2 squares if it's the first move the pawn has ever made. * A pawn may move diagonally if the square is occupied by an opponent piece (capture) * or if the space behind the diagonal is occuped by an opponent pawn that * just moved 2 spaces (en-passant) * +---+---+---+---+---+---+---+---+ * | | | | | | | | | * +---+---+---+---+---+---+---+---+ * | | | | | | | | | * +---+---+---+---+---+---+---+---+ * | | | | | | | | | C = Capture only * +---+---+---+---+---+---+---+---+ T = Move or capture * | | | | M | | | | | M = Move Only * +---+---+---+---+---+---+---+---+ * | | | C | M | C | | | | * +---+---+---+---+---+---+---+---+ * | | | | P | | | | | * +---+---+---+---+---+---+---+---+ * | | | | | | | | | * +---+---+---+---+---+---+---+---+ */ // One rank "forward" which depends on your color int rank = (piece.Color == PieceColor.White) ? piece.Rank + 1 : piece.Rank - 1; if (null == board.FindPieceAt(piece.File, rank)) { moves.Add(new BoardSquare(piece.File, rank)); // The 2nd move is only valid of the 1st one was (i.e. you can't move through // another piece on your first pawn move) if (piece.Deployed == false) { rank += (piece.Color == PieceColor.White) ? 1 : -1; if (null == board.FindPieceAt(piece.File, rank)) { moves.Add(new BoardSquare(piece.File, rank)); } } } // Get the en-passant target if it exists, most of the time it will not // it only exists the move after an enemy pawn has jumped 2 spaces on // its initial move. BoardSquare enPassantTarget; bool enPassantValid = board.GetEnPassantTarget(out enPassantTarget); // Targets will ALWAYS be 1 rank away (enPassant target is behind piece captured) rank = (piece.Color == PieceColor.White) ? piece.Rank + 1 : piece.Rank - 1; // Lambda helper CheckPieceTargets checkPawnTargets = (p, fileOffset, rankOffset, b, m) => { int newFileIndex = p.File.ToInt() + fileOffset; // Can't have diagonal targets on the back rank, or if we're up // against the edge we want to check towards if (!ChessPiece.IsOnBackRank(piece) && (newFileIndex > 0) && (newFileIndex <= 8)) { PieceFile tFile = new PieceFile(p.File.ToInt() + fileOffset); BoardSquare targetSquare = new BoardSquare(tFile, rank); ChessPiece tPiece = b.FindPieceAt(tFile, rank); // Either there is a piece of the opposing color on this square // or the sqaure is a valid enpassant target if (((tPiece != null) && (tPiece.Color != p.Color)) || ((targetSquare == enPassantTarget) && enPassantValid)) { m.Add(new BoardSquare(tFile, rank)); } } }; // There are 2 possible Files (L,R or Kingside, Queenside, etc) // Diagonal left (lower file) PieceFile.ToInt() is 0-based since the // drawing code used it first...so adjust by 1 here checkPawnTargets(piece, -1, 0, board, moves); checkPawnTargets(piece, 1, 0, board, moves); return(moves); }