From 04bfe74ad5fe6bb53eb4d256561f03e6664d5e85 Mon Sep 17 00:00:00 2001 From: Mattia Giambirtone Date: Sun, 21 Apr 2024 15:58:31 +0200 Subject: [PATCH] Add nim.cfg and various bugfixes to movegen --- .gitignore | 1 - Chess/nim.cfg | 5 ++ Chess/nimfish.nimble | 6 +- Chess/nimfish/nimfishpkg/board.nim | 8 +- Chess/nimfish/nimfishpkg/movegen.nim | 113 +++++++++++++++++--------- Chess/nimfish/nimfishpkg/position.nim | 6 +- Chess/nimfish/nimfishpkg/tui.nim | 6 +- 7 files changed, 92 insertions(+), 53 deletions(-) create mode 100644 Chess/nim.cfg diff --git a/.gitignore b/.gitignore index 55f666e..a2252c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ nimcache/ nimblecache/ htmldocs/ -nim.cfg bin # Python __pycache__ diff --git a/Chess/nim.cfg b/Chess/nim.cfg new file mode 100644 index 0000000..9311382 --- /dev/null +++ b/Chess/nim.cfg @@ -0,0 +1,5 @@ +--cc:clang +-o:"bin/nimfish" +-d:danger +--passL:"-flto" +--passC:"-Ofast -flto -march=native -mtune=native" \ No newline at end of file diff --git a/Chess/nimfish.nimble b/Chess/nimfish.nimble index 378cb8d..7937c72 100644 --- a/Chess/nimfish.nimble +++ b/Chess/nimfish.nimble @@ -16,9 +16,5 @@ requires "nim >= 2.0" requires "jsony >= 1.1.5" -after build: - exec "nimble test" - - task test, "Runs the test suite": - exec "python tests/suite.py -d 5 --bulk" + exec "python tests/suite.py -d 6 --bulk" diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim index 0359be0..5126bb4 100644 --- a/Chess/nimfish/nimfishpkg/board.nim +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -43,6 +43,7 @@ type # A bunch of simple utility functions and forward declarations proc toFEN*(self: Chessboard): string +proc updateChecksAndPins*(self: Chessboard) proc newChessboard: Chessboard = @@ -168,6 +169,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard = else: raise newException(ValueError, "invalid FEN: too many fields in FEN string") inc(index) + result.updateChecksAndPins() proc newDefaultChessboard*: Chessboard {.inline.} = @@ -353,13 +355,13 @@ proc updateChecksAndPins*(self: Chessboard) = # Is the pinning ray obstructed by any of our friendly pieces? If so, the # piece is pinned - if (pinningRay and friendlyPieces).countSquares() > 0: + if (pinningRay and friendlyPieces).countSquares() == 1: self.position.diagonalPins = self.position.diagonalPins or pinningRay for piece in canPinOrthogonally: let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() - if (pinningRay and friendlyPieces).countSquares() > 0: - self.position.orthogonalPins = self.position.orthogonalPins or pinningRay + if (pinningRay and friendlyPieces).countSquares() == 1: + self.position.orthogonalPins = self.position.orthogonalPins or pinningRay func inCheck*(self: Chessboard): bool {.inline.} = diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 9a53fae..3dd8437 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +## Move generation logic + import std/strformat @@ -29,16 +31,13 @@ export bitboards, magics, pieces, moves, position, rays, misc, board -proc generatePawnMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = +proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.position.sideToMove pawns = self.getBitboard(Pawn, sideToMove) occupancy = self.getOccupancy() # We can only capture enemy pieces (except the king) enemyPieces = self.getOccupancyFor(sideToMove.opposite()) - # We can only capture diagonally and forward - rightMovement = pawns.forwardRightRelativeTo(sideToMove) - leftMovement = pawns.forwardLeftRelativeTo(sideToMove) epTarget = self.position.enPassantSquare checkers = self.position.checkers diagonalPins = self.position.diagonalPins @@ -61,13 +60,51 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = # If a pawn is pinned diagonally, it cannot move pushablePawns = pawns and not diagonalPins # Neither can it move if it's pinned orthogonally - singlePushes = pushablePawns.forwardRelativeTo(sideToMove) and not occupancy and not orthogonalPins - # Only pawns on their starting rank can double push - doublePushes = (pushablePawns and startingRank).doubleForwardRelativeTo(sideToMove) and not occupancy and orthogonalPins + singlePushes = (pushablePawns.forwardRelativeTo(sideToMove) and not enemyPieces) and destinationMask and not orthogonalPins + # 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 and not orthogonalPins + canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy and destinationMask + + for pawn in singlePushes: + let pawnBB = pawn.toBitboard() + if promotionRank.contains(pawn): + for promotion in [PromoteToBishop, PromoteToBishop, PromoteToQueen, PromoteToRook]: + moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn, promotion)) + else: + moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn)) + + for pawn in canDoublePush: + moves.add(createMove(pawn.toBitboard().doubleBackwardRelativeTo(sideToMove), pawn, DoublePush)) + + let canCapture = pawns and not orthogonalPins + var + captureLeft = canCapture.forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask + captureRight = canCapture.forwardRightRelativeTo(sideToMove) and enemyPieces and destinationMask + # If a capturing pawn is pinned diagonally, it is allowed to capture only + # in the direction of the pin + if (diagonalPins and captureLeft) != 0: + captureLeft = captureLeft and diagonalPins + if (diagonalPins and captureRight) != 0: + captureRight = captureRight and diagonalPins + for pawn in captureLeft: + let pawnBB = pawn.toBitboard() + if promotionRank.contains(pawn): + for promotion in [PromoteToBishop, PromoteToBishop, PromoteToQueen, PromoteToRook]: + moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture, promotion)) + else: + moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture)) + for pawn in captureRight: + let pawnBB = pawn.toBitboard() + if promotionRank.contains(pawn): + for promotion in [PromoteToBishop, PromoteToBishop, PromoteToQueen, PromoteToRook]: + moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture, promotion)) + else: + moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture)) -proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = - ## Helper of generateSlidingMoves to generate rook moves +proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.position.sideToMove occupancy = self.getOccupancy() @@ -82,22 +119,21 @@ proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = let blockers = occupancy and Rook.getRelevantBlockers(square) moveset = getRookMoves(square, blockers) - for target in moveset and not occupancy and pinMask and mask: + 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 mask: + 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 not occupancy and mask: + for target in moveset and destinationMask and not enemyPieces: moves.add(createMove(square, target)) - for target in moveset and enemyPieces and mask: + for target in moveset and enemyPieces and destinationMask: moves.add(createMove(square, target, Capture)) -proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = - ## Helper of generateSlidingMoves to generate bishop moves +proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.position.sideToMove occupancy = self.getOccupancy() @@ -112,28 +148,27 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) let blockers = occupancy and Bishop.getRelevantBlockers(square) moveset = getBishopMoves(square, blockers) - for target in moveset and pinMask and mask: + 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 mask: + for target in moveset 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 mask: + for target in moveset and destinationMask and not enemyPieces: moves.add(createMove(square, target)) - for target in moveset and enemyPieces and mask: + for target in moveset and enemyPieces and destinationMask: moves.add(createMove(square, target, Capture)) proc generateKingMoves(self: Chessboard, moves: var MoveList) = - ## Generates all legal king moves for the side to move let sideToMove = self.position.sideToMove king = self.getBitboard(King, sideToMove) occupancy = self.getOccupancy() nonSideToMove = sideToMove.opposite() - enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) + enemyPieces = self.getOccupancyFor(nonSideToMove) bitboard = getKingAttacks(king.toSquare()) noKingOccupancy = occupancy and not king for square in bitboard and not occupancy: @@ -144,23 +179,29 @@ proc generateKingMoves(self: Chessboard, moves: var MoveList) = moves.add(createMove(king, square, Capture)) -proc generateKnightMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = - ## Generates all the legal knight moves for the side to move +proc generateKnightMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.position.sideToMove knights = self.getBitboard(Knight, sideToMove) nonSideToMove = sideToMove.opposite() pinned = self.position.diagonalPins or self.position.orthogonalPins unpinnedKnights = knights and not pinned - enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) + enemyPieces = self.getOccupancyFor(nonSideToMove) for square in unpinnedKnights: let bitboard = getKnightAttacks(square) - for target in bitboard and mask: + for target in bitboard and destinationMask and not enemyPieces: moves.add(createMove(square, target)) for target in bitboard and enemyPieces: moves.add(createMove(square, target, Capture)) +proc generateCastling(self: Chessboard, moves: var MoveList) = + let + sideToMove = self.position.sideToMove + rooks = self.getBitboard(Rook, sideToMove) + # TODO + + proc generateMoves*(self: Chessboard, moves: var MoveList) = ## Generates the list of all possible legal moves ## in the current position @@ -176,18 +217,17 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = # moves return if not self.inCheck(): - # TODO: Castling - discard + 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 mask: Bitboard + var destinationMask: Bitboard if not self.inCheck(): # Not in check: cannot move over friendly pieces - mask = not self.getOccupancyFor(sideToMove) + 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 @@ -197,12 +237,11 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = # the ray will be empty so the only legal move will be to capture # the checking piece let checker = self.position.checkers.lowestSquare() - mask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard() - - self.generatePawnMoves(moves, mask) - self.generateKnightMoves(moves, mask) - self.generateRookMoves(moves, mask) - self.generateBishopMoves(moves, mask) + destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard() + self.generatePawnMoves(moves, destinationMask) + self.generateKnightMoves(moves, destinationMask) + self.generateRookMoves(moves, destinationMask) + self.generateBishopMoves(moves, destinationMask) # Queens are just handled rooks + bishops @@ -290,7 +329,8 @@ proc doMove*(self: Chessboard, move: Move) = sideToMove: self.position.sideToMove.opposite(), castlingRights: castlingRights, enPassantSquare: enPassantTarget, - pieces: self.position.pieces + pieces: self.position.pieces, + castlingAvailability: self.position.castlingAvailability ) # Update position metadata @@ -336,7 +376,6 @@ proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move # Updates checks and pins for the side to move - self.updateChecksAndPins() if not self.isLegal(move): return nullMove() self.doMove(move) diff --git a/Chess/nimfish/nimfishpkg/position.nim b/Chess/nimfish/nimfishpkg/position.nim index c5db813..29342b0 100644 --- a/Chess/nimfish/nimfishpkg/position.nim +++ b/Chess/nimfish/nimfishpkg/position.nim @@ -22,12 +22,10 @@ type Position* = object ## A chess position - # Castling metadata. Updated on every move - castlingRights*: array[64, uint8] # Castling availability. This just keeps track # of whether the king or the rooks on either side - # moved and is only useful for printing the correct - # FEN for a given position + # moved, the actual checks for the legality of castling + # are done elsewhere castlingAvailability*: tuple[white, black: tuple[queen, king: bool]] # Number of half-moves that were performed # to reach this position starting from the diff --git a/Chess/nimfish/nimfishpkg/tui.nim b/Chess/nimfish/nimfishpkg/tui.nim index 7ea386d..829c244 100644 --- a/Chess/nimfish/nimfishpkg/tui.nim +++ b/Chess/nimfish/nimfishpkg/tui.nim @@ -170,7 +170,7 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) = else: let t = cpuTime() let data = board.perft(ply, divide=true, verbose=verbose) - let tot = cpuTime() + let tot = cpuTime() - t echo &"\nNodes searched (bulk-counting: off): {data.nodes}" echo &" - Captures: {data.captures}" echo &" - Checks: {data.checks}" @@ -179,7 +179,7 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) = echo &" - Castles: {data.castles}" echo &" - Promotions: {data.promotions}" echo "" - echo &"Time taken: {round(t, 3)} seconds\nNodes per second: {round(data.nodes / tot).uint64}" + echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(data.nodes / tot).uint64}" except ValueError: echo "Error: go: perft: invalid depth" else: @@ -217,7 +217,7 @@ proc handleMoveCommand(board: Chessboard, command: seq[string]): Move {.discarda if board.getPiece(targetSquare).kind != Empty: flags.add(Capture) - elif board.getPiece(targetSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: + if board.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: flags.add(DoublePush) if len(moveString) == 5: