/// <summary> /// Calls the valid handler for game over depending on state /// </summary> private void GameOverHandler() { FenParser.DebugFENPosition(board.CurrentFEN); // Full verbose output string[] lines = FenParser.FENToStrings(board.CurrentFEN); StringBuilder sb = new StringBuilder(); foreach (string line in lines) { sb.Append(line); } if (selfPlay) { if (OnChessGameSelfPlayGameOver != null) { OnChessGameSelfPlayGameOver(this, null); } } else { if (OnChessGameNormalPlayGameOver != null) { OnChessGameNormalPlayGameOver(this, null); } } }
/// <summary> /// Sets up the board using the given FEN string. It's possible not all /// pieces are present. /// </summary> /// <param name="fen">FEN string that describes the position /// (https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)</param> private void CreateAndPlacePieces(string fen) { Reset(); // String starts on the back rank on the A file then moves Left->Right // Top->Bottom string fenString = fen.Trim(); activePlayer = FenParser.ExtractActivePlayer(fenString); FenParser.ExtractCastlingRights(fenString, ref whiteCastlingRights, ref blackCastlingRights); enPassantValid = FenParser.ExtractEnPassantTarget(fenString, out enPassantTarget); FenParser.ExtractMoveCounts(fenString, ref halfMoveCount, ref fullMoveCount); List <ChessPiece> pieces = FenParser.ExtractPieces(fenString); foreach (ChessPiece fenPiece in pieces) { // For pawns and rooks and kings, deployment matters, check if they're on their home // rank/square and if not, set it to true if (fenPiece.Job == PieceClass.Pawn) { if (PawnHomeRank(fenPiece.Color) != fenPiece.Rank) { fenPiece.Deployed = true; } } else if (fenPiece.Job == PieceClass.Rook) { if (((fenPiece.File.ToInt() != 1) && (fenPiece.File.ToInt() != 8)) || (fenPiece.Rank != RookHomeRank(fenPiece.Color))) { fenPiece.Deployed = true; } } else if (fenPiece.Job == PieceClass.King) { // Both colors should be on the E file and their home rank int homeRank = (fenPiece.Color == PieceColor.White) ? 1 : 8; if ((fenPiece.Rank != homeRank) || (fenPiece.File != new PieceFile('e'))) { fenPiece.Deployed = true; } } if (fenPiece.Color == PieceColor.White) { whitePieces.Add(fenPiece); } else { blackPieces.Add(fenPiece); } } }
/// <summary> /// Move a piece from startFile:startRank -> targetFile:targetRank. Because /// self-play is the only mode enabled right now, these moves are always /// going to be considered valid, since they came from the chess engine /// (and we will assume it is correct). In the future, this will likely /// remain, and validation of the legallity for the player can be handled /// above this call /// </summary> /// <param name="moveInfo">detailed move information struct</param> public void MovePiece(ref MoveInformation moveInfo) { // Get the player piece at the starting location // Piece should never be null if chess logic is sound PieceFile startFile = moveInfo.Start.File; int startRank = moveInfo.Start.Rank; PieceFile targetFile = moveInfo.End.File; int targetRank = moveInfo.End.Rank; ChessPiece playerPiece = FindPieceAt(startFile, startRank); if (playerPiece.Color != activePlayer) { // This also should not be able to happen with correct game logic throw new InvalidOperationException(); } // Get each side's pieces List <ChessPiece> playerPieces = ActivePlayerPieces; List <ChessPiece> opponentPieces = OpponentPlayerPieces; // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // PRE-MOVE CHECKS // We have to detect castling. It does not come across the wire as O-O or O-O-O // but rather as a regular move like e1g1. Separate the detection from the move // of the rook bool isCastling = IsCastling(playerPiece, targetFile); // We also need to check for an en-passant capture if the pieces is a pawn if (playerPiece.Job == PieceClass.Pawn) { moveInfo.CapturedPiece = HandleEnPassant(startFile, startRank, targetFile, targetRank); } // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // RAW MOVE(S) playerPiece.Move(targetFile, targetRank); if (isCastling) { PerformCastle(targetFile, ref moveInfo); // Also move the rook if needed } // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // POST-MOVE CHECKS/UPDATES // For normal captures, just do a quick iteration of the opponent pieces // there are only 16 of these total in normal chess foreach (ChessPiece enemyPiece in opponentPieces) { if ((enemyPiece.Rank == targetRank) && // Enemy piece is located in (enemyPiece.File == targetFile) && // the square we just moved to !enemyPiece.Captured) // and it's not already captured { enemyPiece.Captured = true; // Stop drawing it (capture) moveInfo.CapturedPiece = enemyPiece; // Record the capture break; // exit the search loop } } // save the last capture state for external callers lastMoveWasCapture = moveInfo.IsCapture; Moves.Add(moveInfo); // Update our FEN currentFEN = FenParser.ApplyMoveToFEN(currentFEN, moveInfo.ToString()); enPassantValid = FenParser.ExtractEnPassantTarget(currentFEN, out enPassantTarget); FenParser.ExtractCastlingRights(CurrentFEN, ref whiteCastlingRights, ref blackCastlingRights); FenParser.ExtractMoveCounts(CurrentFEN, ref halfMoveCount, ref fullMoveCount); // Flip players - easy to just do here rather than parse the FEN again activePlayer = OppositeColor(activePlayer); }
/// <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); }