Add nim.cfg and various bugfixes to movegen

This commit is contained in:
Mattia Giambirtone 2024-04-21 15:58:31 +02:00
parent 68c170568e
commit 04bfe74ad5
7 changed files with 92 additions and 53 deletions

1
.gitignore vendored
View File

@ -2,7 +2,6 @@
nimcache/
nimblecache/
htmldocs/
nim.cfg
bin
# Python
__pycache__

5
Chess/nim.cfg Normal file
View File

@ -0,0 +1,5 @@
--cc:clang
-o:"bin/nimfish"
-d:danger
--passL:"-flto"
--passC:"-Ofast -flto -march=native -mtune=native"

View File

@ -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"

View File

@ -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.} =

View File

@ -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)

View File

@ -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

View File

@ -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: