# Copyright 2024 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ## Move generation logic import std/strformat import std/strutils import std/tables import bitboards import board import magics import pieces import moves import position import rays import see export bitboards, magics, pieces, moves, position, rays, board proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.sideToMove nonSideToMove = sideToMove.opposite() pawns = self.getBitboard(Pawn, sideToMove) occupancy = self.getOccupancy() # We can only capture enemy pieces enemyPieces = self.getOccupancyFor(nonSideToMove) epTarget = self.enPassantSquare diagonalPins = self.diagonalPins orthogonalPins = self.orthogonalPins promotionRank = sideToMove.getLastRank() startingRank = sideToMove.getSecondRank() friendlyKing = self.getBitboard(King, sideToMove).toSquare() # Single and double pushes # If a pawn is pinned diagonally, it cannot push forward let # If a pawn is pinned horizontally, it cannot move either. It can move vertically # though. Thanks to Twipply for the tip on how to get a horizontal pin mask out of # our orthogonal bitboard :) horizontalPins = Bitboard((0xFF'u64 shl (rankFromSquare(friendlyKing).uint64 * 8))) and orthogonalPins pushablePawns = pawns and not diagonalPins and not horizontalPins singlePushes = (pushablePawns.forwardRelativeTo(sideToMove) and not occupancy) and destinationMask # We do this weird dance instead of using doubleForwardRelativeTo() because that doesn't have any # way to check if there's pieces on the two squares ahead of the pawn var canDoublePush = pushablePawns and startingRank canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy and destinationMask for pawn in singlePushes and not promotionRank: moves.add(createMove(pawn.toBitboard().backwardRelativeTo(sideToMove), pawn)) for pawn in singlePushes and promotionRank: for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: moves.add(createMove(pawn.toBitboard().backwardRelativeTo(sideToMove), pawn, promotion)) for pawn in canDoublePush: moves.add(createMove(pawn.toBitboard().doubleBackwardRelativeTo(sideToMove), pawn, DoublePush)) let canCapture = pawns and not orthogonalPins canCaptureLeftUnpinned = (canCapture and not diagonalPins).forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask canCaptureRightUnpinned = (canCapture and not diagonalPins).forwardRightRelativeTo(sideToMove) and enemyPieces and destinationMask for pawn in canCaptureRightUnpinned and not promotionRank: moves.add(createMove(pawn.toBitboard().backwardLeftRelativeTo(sideToMove), pawn, Capture)) for pawn in canCaptureRightUnpinned and promotionRank: for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: moves.add(createMove(pawn.toBitboard().backwardLeftRelativeTo(sideToMove), pawn, Capture, promotion)) for pawn in canCaptureLeftUnpinned and not promotionRank: moves.add(createMove(pawn.toBitboard().backwardRightRelativeTo(sideToMove), pawn, Capture)) for pawn in canCaptureLeftUnpinned and promotionRank: for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: moves.add(createMove(pawn.toBitboard().backwardRightRelativeTo(sideToMove), pawn, Capture, promotion)) # Special cases for pawns pinned diagonally that can capture their pinners let canCaptureLeft = canCapture.forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask canCaptureRight = canCapture.forwardRightRelativeTo(sideToMove) and enemyPieces and destinationMask leftPinnedCanCapture = (canCaptureLeft and diagonalPins) and not canCaptureLeftUnpinned rightPinnedCanCapture = ((canCaptureRight and diagonalPins) and not canCaptureRightUnpinned) and not canCaptureRightUnpinned for pawn in leftPinnedCanCapture and not promotionRank: moves.add(createMove(pawn.toBitboard().backwardRightRelativeTo(sideToMove), pawn, Capture)) for pawn in leftPinnedCanCapture and promotionRank: for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: moves.add(createMove(pawn.toBitboard().backwardRightRelativeTo(sideToMove), pawn, Capture, promotion)) for pawn in rightPinnedCanCapture and not promotionRank: moves.add(createMove(pawn.toBitboard().backwardLeftRelativeTo(sideToMove), pawn, Capture)) for pawn in rightPinnedCanCapture and promotionRank: for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: moves.add(createMove(pawn.toBitboard().backwardLeftRelativeTo(sideToMove), pawn, Capture, promotion)) # En passant captures var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0) if epBitboard != 0: # See if en passant would create a check let # We don't and the destination mask with the ep target because we already check # whether the king ends up in check. TODO: Fix this in a more idiomatic way epPawn = epBitboard.backwardRelativeTo(sideToMove) epLeft = pawns.forwardLeftRelativeTo(sideToMove) and epBitboard epRight = pawns.forwardRightRelativeTo(sideToMove) and epBitboard # Note: it's possible for two pawns to both have rights to do an en passant! See # 4k3/8/8/2PpP3/8/8/8/4K3 w - d6 0 1 if epLeft != 0: # We basically simulate the en passant and see if the resulting # occupancy bitboard has the king in check let friendlyPawn = epBitboard.backwardRightRelativeTo(sideToMove) newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard # We also need to temporarily remove the en passant pawn from # our bitboards, or else functions like getPawnAttacks won't # get the news that the pawn is gone and will still think the # king is in check after en passant when it actually isn't # (see pos fen rnbqkbnr/pppp1ppp/8/2P5/K7/8/PPPP1PPP/RNBQ1BNR b kq - 0 1 moves b7b5 c5b6) let epPawnSquare = epPawn.toSquare() let epPiece = self.getPiece(epPawnSquare) self.removePiece(epPawnSquare) if not self.isOccupancyAttacked(friendlyKing, newOccupancy): # En passant does not create a check on the king: all good moves.add(createMove(friendlyPawn, epBitboard, EnPassant)) self.spawnPiece(epPawnSquare, epPiece) if epRight != 0: # Note that this isn't going to be the same pawn from the previous if block! let friendlyPawn = epBitboard.backwardLeftRelativeTo(sideToMove) newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard let epPawnSquare = epPawn.toSquare() let epPiece = self.getPiece(epPawnSquare) self.removePiece(epPawnSquare) if not self.isOccupancyAttacked(friendlyKing, newOccupancy): # En passant does not create a check on the king: all good moves.add(createMove(friendlyPawn, epBitboard, EnPassant)) self.spawnPiece(epPawnSquare, epPiece) proc generateRookMoves(self: Position, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.sideToMove occupancy = self.getOccupancy() enemyPieces = self.getOccupancyFor(sideToMove.opposite()) rooks = self.getBitboard(Rook, sideToMove) queens = self.getBitboard(Queen, sideToMove) movableRooks = not self.diagonalPins and (queens or rooks) pinMask = self.orthogonalPins pinnedRooks = movableRooks and pinMask unpinnedRooks = movableRooks and not pinnedRooks for square in pinnedRooks: let blockers = occupancy and Rook.getRelevantBlockers(square) moveset = getRookMoves(square, blockers) for target in moveset and pinMask and destinationMask and not enemyPieces: moves.add(createMove(square, target)) for target in moveset and enemyPieces and pinMask and destinationMask: moves.add(createMove(square, target, Capture)) for square in unpinnedRooks: let blockers = occupancy and Rook.getRelevantBlockers(square) moveset = getRookMoves(square, blockers) for target in moveset and destinationMask and not enemyPieces: moves.add(createMove(square, target)) for target in moveset and enemyPieces and destinationMask: moves.add(createMove(square, target, Capture)) proc generateBishopMoves(self: Position, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.sideToMove occupancy = self.getOccupancy() enemyPieces = self.getOccupancyFor(sideToMove.opposite()) bishops = self.getBitboard(Bishop, sideToMove) queens = self.getBitboard(Queen, sideToMove) movableBishops = not self.orthogonalPins and (queens or bishops) pinMask = self.diagonalPins pinnedBishops = movableBishops and pinMask unpinnedBishops = movableBishops and not pinnedBishops for square in pinnedBishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) moveset = getBishopMoves(square, blockers) for target in moveset and pinMask and destinationMask and not enemyPieces: moves.add(createMove(square, target)) for target in moveset and pinMask and enemyPieces and destinationMask: moves.add(createMove(square, target, Capture)) for square in unpinnedBishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) moveset = getBishopMoves(square, blockers) for target in moveset and destinationMask and not enemyPieces: moves.add(createMove(square, target)) for target in moveset and enemyPieces and destinationMask: moves.add(createMove(square, target, Capture)) proc generateKingMoves(self: Position, moves: var MoveList, capturesOnly=false) = let sideToMove = self.sideToMove king = self.getBitboard(King, sideToMove) occupancy = self.getOccupancy() nonSideToMove = sideToMove.opposite() enemyPieces = self.getOccupancyFor(nonSideToMove) bitboard = getKingAttacks(king.toSquare()) noKingOccupancy = occupancy and not king if not capturesOnly: for square in bitboard and not occupancy: if not self.isOccupancyAttacked(square, noKingOccupancy): moves.add(createMove(king, square)) for square in bitboard and enemyPieces: if not self.isOccupancyAttacked(square, noKingOccupancy): moves.add(createMove(king, square, Capture)) proc generateKnightMoves(self: Position, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.sideToMove knights = self.getBitboard(Knight, sideToMove) nonSideToMove = sideToMove.opposite() pinned = self.diagonalPins or self.orthogonalPins unpinnedKnights = knights and not pinned enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) for square in unpinnedKnights: let bitboard = getKnightAttacks(square) for target in bitboard and destinationMask and not enemyPieces: moves.add(createMove(square, target)) for target in bitboard and destinationMask and enemyPieces: moves.add(createMove(square, target, Capture)) proc generateCastling(self: Position, moves: var MoveList) = let sideToMove = self.sideToMove castlingRights = self.canCastle() kingSquare = self.getBitboard(King, sideToMove).toSquare() kingPiece = self.getPiece(kingSquare) if castlingRights.king: moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle)) if castlingRights.queen: moves.add(createMove(kingSquare, kingPiece.queenSideCastling(), Castle)) proc generateMoves*(self: var Position, moves: var MoveList, capturesOnly: bool = false) = ## Generates the list of all possible legal moves ## in the current position. If capturesOnly is ## true, only capture moves are generated let sideToMove = self.sideToMove nonSideToMove = sideToMove.opposite() self.generateKingMoves(moves, capturesOnly) if self.checkers.countSquares() > 1: # King is in double check: no need to generate any more # moves return self.generateCastling(moves) # We pass a mask to our move generators to remove stuff # like our friendly pieces from the set of possible # target squares, as well as to ensure checks are not # ignored var destinationMask: Bitboard if not self.inCheck(): # Not in check: cannot move over friendly pieces destinationMask = not self.getOccupancyFor(sideToMove) else: # We *are* in check (from a single piece, because the two checks # case was handled above already). If the piece is a slider, we'll # extract the ray from it to our king and add the checking piece to # it, meaning the only legal moves are those that either block the # check or capture the checking piece. For other non-sliding pieces # the ray will be empty so the only legal move will be to capture # the checking piece (or moving the king) let checker = self.checkers.lowestSquare() checkerBB = checker.toBitboard() # epTarget = self.position.enPassantSquare # checkerPiece = self.position.getPiece(checker) destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB # TODO: This doesn't really work. I've addressed the issue for now, but it's kinda ugly. Find a better # solution # if checkerPiece.kind == Pawn and checkerBB.backwardRelativeTo(checkerPiece.color).toSquare() == epTarget: # # We are in check by a pawn that pushed two squares: add the ep target square to the set of # # squares that our friendly pieces can move to in order to resolve it. This will do nothing # # for most pieces, because the move generators won't allow them to move there, but it does matter # # for pawns # destinationMask = destinationMask or epTarget.toBitboard() if capturesOnly: # Note: This does not cover en passant (which is good because it's a capture, # but the "fix" stands on flimsy ground) destinationMask = destinationMask and self.getOccupancyFor(nonSideToMove) self.generatePawnMoves(moves, destinationMask) self.generateKnightMoves(moves, destinationMask) self.generateRookMoves(moves, destinationMask) self.generateBishopMoves(moves, destinationMask) # Queens are just handled rooks + bishops proc generateMoves*(self: var Chessboard, moves: var MoveList, capturesOnly=false) = ## The same as Position.generateMoves() self.position.generateMoves(moves, capturesOnly) proc doMove*(self: var Chessboard, move: Move) = ## Internal function called by makeMove after ## performing legality checks. Can be used in ## performance-critical paths where a move is ## already known to be legal (i.e. during search) # Record final position for future reference self.positions.add(self.position) # Final checks let piece = self.position.getPiece(move.startSquare) sideToMove = piece.color nonSideToMove = sideToMove.opposite() when not defined(danger): doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}" var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount enPassantTarget = nullSquare() if self.position.enPassantSquare != nullSquare(): # Unset the previous en passant square in the zobrist key self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(self.position.enPassantSquare)) # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): # Number of half-moves since the last reversible half-move halfMoveClock = 0 else: inc(halfMoveClock) if piece.color == Black: inc(fullMoveCount) if move.isDoublePush(): enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() # Create new position self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, sideToMove: nonSideToMove, enPassantSquare: enPassantTarget, pieces: self.position.pieces, castlingAvailability: self.position.castlingAvailability, zobristKey: self.position.zobristKey, mailbox: self.position.mailbox ) if self.position.enPassantSquare != nullSquare(): self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(move.targetSquare)) # Update position metadata if move.isEnPassant(): # Make the en passant pawn disappear let epPawnSquare = move.targetSquare.toBitboard().backwardRelativeTo(sideToMove).toSquare() self.position.zobristKey = self.position.zobristKey xor self.position.getPiece(epPawnSquare).getKey(epPawnSquare) self.position.removePiece(epPawnSquare) if move.isCastling() or piece.kind == King: # If the king has moved, all castling rights for the side to # move are revoked if self.position.castlingAvailability[sideToMove].king: # XOR is its own inverse, so while setting a boolean to false more than once # is not a problem, XORing the same key twice would give back the castling # rights to the moving side! self.position.castlingAvailability[sideToMove].king = false self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(sideToMove) if self.position.castlingAvailability[sideToMove].queen: self.position.castlingAvailability[sideToMove].queen = false self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(sideToMove) if move.isCastling(): # Move the rook where it belongs var source: Square rook: Piece target: Square if move.targetSquare == piece.kingSideCastling(): source = sideToMove.kingSideRook() rook = self.position.getPiece(source) target = rook.kingSideCastling() elif move.targetSquare == piece.queenSideCastling(): source = sideToMove.queenSideRook() rook = self.position.getPiece(source) target = rook.queenSideCastling() self.position.movePiece(source, target) self.position.zobristKey = self.position.zobristKey xor rook.getKey(source) self.position.zobristKey = self.position.zobristKey xor rook.getKey(target) if piece.kind == Rook: # If a rook on either side moves, castling rights are permanently revoked # on that side if move.startSquare == sideToMove.kingSideRook(): if self.position.castlingAvailability[sideToMove].king: self.position.castlingAvailability[sideToMove].king = false self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(sideToMove) elif move.startSquare == sideToMove.queenSideRook(): if self.position.castlingAvailability[sideToMove].queen: self.position.castlingAvailability[sideToMove].queen = false self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(sideToMove) if move.isCapture(): # Get rid of captured pieces let captured = self.position.getPiece(move.targetSquare) self.position.zobristKey = self.position.zobristKey xor captured.getKey(move.targetSquare) self.position.removePiece(move.targetSquare) # If a rook has been captured, castling on that side is prohibited if captured.kind == Rook: if move.targetSquare == captured.color.kingSideRook(): if self.position.castlingAvailability[nonSideToMove].king: self.position.castlingAvailability[nonSideToMove].king = false self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(nonSideToMove) elif move.targetSquare == captured.color.queenSideRook(): if self.position.castlingAvailability[nonSideToMove].queen: self.position.castlingAvailability[nonSideToMove].queen = false self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(nonSideToMove) # Move the piece to its target square self.position.movePiece(move) # Update piece location in the zobrist hash self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.startSquare) self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare) if move.isPromotion(): # Move is a pawn promotion: get rid of the pawn # and spawn a new piece self.position.removePiece(move.targetSquare) self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare) var spawnedPiece: Piece case move.getPromotionType(): of PromoteToBishop: spawnedPiece = Piece(kind: Bishop, color: piece.color) of PromoteToKnight: spawnedPiece = Piece(kind: Knight, color: piece.color) of PromoteToRook: spawnedPiece = Piece(kind: Rook, color: piece.color) of PromoteToQueen: spawnedPiece = Piece(kind: Queen, color: piece.color) else: # Unreachable discard self.position.zobristKey = self.position.zobristKey xor spawnedPiece.getKey(move.targetSquare) self.position.spawnPiece(move.targetSquare, spawnedPiece) # Updates checks and pins for the new side to move self.position.updateChecksAndPins() # Swap the side to move self.position.zobristKey = self.position.zobristKey xor getBlackToMoveKey() proc isLegal*(self: var Chessboard, move: Move): bool {.inline.} = ## Returns whether the given move is legal var moves = newMoveList() self.generateMoves(moves) return move in moves proc isLegal*(self: var Position, move: Move): bool {.inline.} = ## Returns whether the given move is legal var moves = newMoveList() self.generateMoves(moves) return move in moves proc makeMove*(self: var Chessboard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move # Updates checks and pins for the side to move if not self.isLegal(move): return nullMove() self.doMove(move) proc makeNullMove*(self: var Chessboard) = ## Makes a "null" move, i.e. passes the turn ## to the opponent without making a move. This ## is obviously illegal and only to be used during ## search. The move can be undone via unmakeMove self.positions.add(self.position) self.position.sideToMove = self.position.sideToMove.opposite() self.position.enPassantSquare = nullSquare() self.position.fromNull = true self.position.updateChecksAndPins() self.position.hash() proc canNullMove*(self: Chessboard): bool = ## Returns whether a null move can be made. ## Specifically, one cannot null move if a ## null move was already made previously or ## if the side to move is in check return not self.inCheck() and not self.position.fromNull proc unmakeMove*(self: var Chessboard) = ## Reverts to the previous board position if self.positions.len() == 0: return self.position = self.positions.pop() ## Testing stuff proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) = doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead" proc testPieceCount(board: Chessboard, kind: PieceKind, color: PieceColor, count: int) = let pieces = board.position.countPieces(kind, color) doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead" proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) = var i = 0 for square in bitboard: doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}" inc(i) if i != squares.len(): doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})" ## Tests const testFens = staticRead("../../tests/all.txt").splitLines() const drawnFens = [("4k3/2b5/8/8/8/5B2/8/4K3 w - - 0 1", false), # KBvKB (currently not handled) ("4k3/2b5/8/8/8/8/8/4K3 w - - 0 1", true), # KBvK ("4k3/8/6b1/8/8/8/8/4K3 w - - 0 1", true), # KvKB ("4k3/8/8/6N1/8/8/8/4K3 w - - 0 1", true), # KNvK ("4k3/8/8/5n2/8/8/8/4K3 w - - 0 1", true), # KvKN ("4k3/8/8/5n2/8/5N2/8/4K3 w - - 0 1", false), # KNvKN ("4k3/8/6b1/7b/8/8/8/4K3 w - - 0 1", false), # KvKBB with same color bishop (currently not handled) ("4k3/8/8/5B2/6B1/8/8/4K3 w - - 0 1", false) # KBBvK with same color bishop (currently not handled) ] const seeFens = [("4R3/2r3p1/5bk1/1p1r3p/p2PR1P1/P1BK1P2/1P6/8 b - - 0 1", createMove("h5", "g4", Capture), 0), ("4R3/2r3p1/5bk1/1p1r1p1p/p2PR1P1/P1BK1P2/1P6/8 b - - 0 1", createMove("h5", "g4", Capture), 0), ("4r1k1/5pp1/nbp4p/1p2p2q/1P2P1b1/1BP2N1P/1B2QPPK/3R4 b - - 0 1", createMove("g4", "f3", Capture), Knight.getStaticPieceScore() - Bishop.getStaticPieceScore()), ("2r1r1k1/pp1bppbp/3p1np1/q3P3/2P2P2/1P2B3/P1N1B1PP/2RQ1RK1 b - - 0 1", createMove("d6", "e5", Capture) , Pawn.getStaticPieceScore()), ("7r/5qpk/p1Qp1b1p/3r3n/BB3p2/5p2/P1P2P2/4RK1R w - - 0 1", createMove("e1", "e8"), 0), ("6rr/6pk/p1Qp1b1p/2n5/1B3p2/5p2/P1P2P2/4RK1R w - - 0 1", createMove("e1", "e8"), -Rook.getStaticPieceScore()), ("7r/5qpk/2Qp1b1p/1N1r3n/BB3p2/5p2/P1P2P2/4RK1R w - - 0 1", createMove("e1", "e8"), -Rook.getStaticPieceScore()), ("6RR/4bP2/8/8/5r2/3K4/5p2/4k3 w - - 0 1", createMove("f7", "f8", PromoteToQueen), Bishop.getStaticPieceScore() - Pawn.getStaticPieceScore()), ("6RR/4bP2/8/8/5r2/3K4/5p2/4k3 w - - 0 1", createMove("f7", "f8", PromoteToKnight), Knight.getStaticPieceScore() - Pawn.getStaticPieceScore()), ("7R/4bP2/8/8/1q6/3K4/5p2/4k3 w - - 0 1", createMove("f7", "f8", PromoteToRook), -Pawn.getStaticPieceScore()), ("8/4kp2/2npp3/1Nn5/1p2PQP1/7q/1PP1B3/4KR1r b - - 0 1", createMove("h1", "f1", Capture), 0), ("8/4kp2/2npp3/1Nn5/1p2P1P1/7q/1PP1B3/4KR1r b - - 0 1", createMove("h1", "f1", Capture), 0), ("2r2r1k/6bp/p7/2q2p1Q/3PpP2/1B6/P5PP/2RR3K b - - 0 1", createMove("c5", "c1", Capture), 2 * Rook.getStaticPieceScore() - Queen.getStaticPieceScore()), ("r2qk1nr/pp2ppbp/2b3p1/2p1p3/8/2N2N2/PPPP1PPP/R1BQR1K1 w qk - 0 1", createMove("f3", "e5", Capture), Pawn.getStaticPieceScore()), ("6r1/4kq2/b2p1p2/p1pPb3/p1P2B1Q/2P4P/2B1R1P1/6K1 w - - 0 1", createMove("f4", "e5", Capture), 0), ("3q2nk/pb1r1p2/np6/3P2Pp/2p1P3/2R4B/PQ3P1P/3R2K1 w - h6 0 1", createMove("g5", "h6", EnPassant), 0), ("3q2nk/pb1r1p2/np6/3P2Pp/2p1P3/2R1B2B/PQ3P1P/3R2K1 w - h6 0 1", createMove("g5", "h6", EnPassant), Pawn.getStaticPieceScore()), ("2r4r/1P4pk/p2p1b1p/7n/BB3p2/2R2p2/P1P2P2/4RK2 w - - 0 1", createMove("c3", "c8", Capture), Rook.getStaticPieceScore()), ("2r5/1P4pk/p2p1b1p/5b1n/BB3p2/2R2p2/P1P2P2/4RK2 w - - 0 1", createMove("c3", "c8", Capture), Rook.getStaticPieceScore()), ("2r4k/2r4p/p7/2b2p1b/4pP2/1BR5/P1R3PP/2Q4K w - - 0 1", createMove("c3", "c5", Capture), Bishop.getStaticPieceScore()), ("8/pp6/2pkp3/4bp2/2R3b1/2P5/PP4B1/1K6 w - - 0 1", createMove("g2", "c6", Capture), Pawn.getStaticPieceScore() - Bishop.getStaticPieceScore()), ("4q3/1p1pr1k1/1B2rp2/6p1/p3PP2/P3R1P1/1P2R1K1/4Q3 b - - 0 1", createMove("e6", "e4", Capture), Pawn.getStaticPieceScore()-Rook.getStaticPieceScore()), ("4q3/1p1pr1kb/1B2rp2/6p1/p3PP2/P3R1P1/1P2R1K1/4Q3 b - - 0 1", createMove("h7", "e4", Capture), Pawn.getStaticPieceScore()), ("r1q1r1k1/pb1nppbp/1p3np1/1Pp1N3/3pNP2/B2P2PP/P3P1B1/2R1QRK1 w - c6 0 11", createMove("b5", "c6", EnPassant), Pawn.getStaticPieceScore()), ("r3k2r/p1ppqpb1/Bn2pnp1/3PN3/1p2P3/2N2Q2/PPPB1PpP/R3K2R w QKqk - 0 2", createMove("a6", "f1"), Pawn.getStaticPieceScore() - Bishop.getStaticPieceScore()) ] proc basicTests* = # Test the FEN parser for fen in testFens: doAssert fen == loadFEN(fen).toFEN() # Test zobrist hashing for fen in testFens: var board = newChessboardFromFEN(fen) hashes = newTable[ZobristKey, Move]() moves = newMoveList() board.generateMoves(moves) for move in moves: board.makeMove(move) let pos = board.position key = pos.zobristKey board.unmakeMove() doAssert not hashes.contains(key), &"{fen} has zobrist collisions {move} -> {hashes[key]} (key is {key.uint64})" hashes[key] = move # Test detection of (some) draws by insufficient material for (fen, isDrawn) in drawnFens: doAssert newChessboardFromFEN(fen).isInsufficientMaterial() == isDrawn, &"draw check failed for {fen} (expected {isDrawn})" # Test SEE scores for (fen, move, expected) in seeFens: let res = loadFEN(fen).see(move) doAssert res == expected, &"SEE test failed for {fen} ({move}): expected {expected}, got {res}" var board = newDefaultChessboard() # Ensure correct number of pieces testPieceCount(board, Pawn, White, 8) testPieceCount(board, Pawn, Black, 8) testPieceCount(board, Knight, White, 2) testPieceCount(board, Knight, Black, 2) testPieceCount(board, Bishop, White, 2) testPieceCount(board, Bishop, Black, 2) testPieceCount(board, Rook, White, 2) testPieceCount(board, Rook, Black, 2) testPieceCount(board, Queen, White, 1) testPieceCount(board, Queen, Black, 1) testPieceCount(board, King, White, 1) testPieceCount(board, King, Black, 1) # Ensure pieces are in the correct squares # Pawns for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]: testPiece(board.position.getPiece(loc), Pawn, White) for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]: testPiece(board.position.getPiece(loc), Pawn, Black) # Rooks testPiece(board.position.getPiece("a1"), Rook, White) testPiece(board.position.getPiece("h1"), Rook, White) testPiece(board.position.getPiece("a8"), Rook, Black) testPiece(board.position.getPiece("h8"), Rook, Black) # Knights testPiece(board.position.getPiece("b1"), Knight, White) testPiece(board.position.getPiece("g1"), Knight, White) testPiece(board.position.getPiece("b8"), Knight, Black) testPiece(board.position.getPiece("g8"), Knight, Black) # Bishops testPiece(board.position.getPiece("c1"), Bishop, White) testPiece(board.position.getPiece("f1"), Bishop, White) testPiece(board.position.getPiece("c8"), Bishop, Black) testPiece(board.position.getPiece("f8"), Bishop, Black) # Kings testPiece(board.position.getPiece("e1"), King, White) testPiece(board.position.getPiece("e8"), King, Black) # Queens testPiece(board.position.getPiece("d1"), Queen, White) testPiece(board.position.getPiece("d8"), Queen, Black) # Ensure our bitboards match with the board let whitePawns = board.position.getBitboard(Pawn, White) whiteKnights = board.position.getBitboard(Knight, White) whiteBishops = board.position.getBitboard(Bishop, White) whiteRooks = board.position.getBitboard(Rook, White) whiteQueens = board.position.getBitboard(Queen, White) whiteKing = board.position.getBitboard(King, White) blackPawns = board.position.getBitboard(Pawn, Black) blackKnights = board.position.getBitboard(Knight, Black) blackBishops = board.position.getBitboard(Bishop, Black) blackRooks = board.position.getBitboard(Rook, Black) blackQueens = board.position.getBitboard(Queen, Black) blackKing = board.position.getBitboard(King, Black) whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)] whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)] whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)] whiteRookSquares = @[makeSquare(7'i8, 0'i8), makeSquare(7, 7)] whiteQueenSquares = @[makeSquare(7'i8, 3'i8)] whiteKingSquares = @[makeSquare(7'i8, 4'i8)] blackPawnSquares = @[makeSquare(1'i8, 0'i8), makeSquare(1, 1), makeSquare(1, 2), makeSquare(1, 3), makeSquare(1, 4), makeSquare(1, 5), makeSquare(1, 6), makeSquare(1, 7)] blackKnightSquares = @[makeSquare(0'i8, 1'i8), makeSquare(0, 6)] blackBishopSquares = @[makeSquare(0'i8, 2'i8), makeSquare(0, 5)] blackRookSquares = @[makeSquare(0'i8, 0'i8), makeSquare(0, 7)] blackQueenSquares = @[makeSquare(0'i8, 3'i8)] blackKingSquares = @[makeSquare(0'i8, 4'i8)] testPieceBitboard(whitePawns, whitePawnSquares) testPieceBitboard(whiteKnights, whiteKnightSquares) testPieceBitboard(whiteBishops, whiteBishopSquares) testPieceBitboard(whiteRooks, whiteRookSquares) testPieceBitboard(whiteQueens, whiteQueenSquares) testPieceBitboard(whiteKing, whiteKingSquares) testPieceBitboard(blackPawns, blackPawnSquares) testPieceBitboard(blackKnights, blackKnightSquares) testPieceBitboard(blackBishops, blackBishopSquares) testPieceBitboard(blackRooks, blackRookSquares) testPieceBitboard(blackQueens, blackQueenSquares) testPieceBitboard(blackKing, blackKingSquares) # Test repetition for move in ["b1c3", "g8f6", "c3b1", "f6g8", "b1c3", "g8f6", "c3b1", "f6g8"]: board.makeMove(createMove(move[0..1].toSquare(), move[2..3].toSquare())) doAssert board.drawnByRepetition()