Make test suite optionally parallel. Many bug fixes

This commit is contained in:
Mattia Giambirtone 2024-04-23 01:50:56 +02:00
parent 04bfe74ad5
commit 0dfd647f4c
14 changed files with 411 additions and 151 deletions

View File

@ -2,4 +2,4 @@
-o:"bin/nimfish"
-d:danger
--passL:"-flto"
--passC:"-Ofast -flto -march=native -mtune=native"
--passC:"-Ofast -flto -march=native -mtune=native"

View File

@ -130,13 +130,13 @@ proc newChessboardFromFEN*(fen: string): Chessboard =
of '-':
discard
of 'K':
result.position.castlingAvailability.white.king = true
result.position.castlingAvailability[White.int].king = true
of 'Q':
result.position.castlingAvailability.white.queen = true
result.position.castlingAvailability[White.int].queen = true
of 'k':
result.position.castlingAvailability.black.king = true
result.position.castlingAvailability[Black.int].king = true
of 'q':
result.position.castlingAvailability.black.queen = true
result.position.castlingAvailability[Black.int].queen = true
else:
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section")
of 3:
@ -230,7 +230,8 @@ func getKingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bi
result = Bitboard(0)
let
king = self.getBitboard(King, attacker)
if (getKingAttacks(square) and king) != 0:
squareBB = square.toBitboard()
if (getKingAttacks(square) and squareBB) != 0:
result = result or king
@ -238,11 +239,11 @@ func getKnightAttacks*(self: Chessboard, square: Square, attacker: PieceColor):
## Returns the locations of the knights attacking the given square
let
knights = self.getBitboard(Knight, attacker)
squareBB = square.toBitboard()
result = Bitboard(0)
for knight in knights:
let knightBB = knight.toBitboard()
if (getKnightAttacks(knight) and knightBB) != 0:
result = result or knightBB
if (getKnightAttacks(knight) and squareBB) != 0:
result = result or knight.toBitboard()
proc getSlidingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
@ -369,10 +370,42 @@ func inCheck*(self: Chessboard): bool {.inline.} =
return self.position.checkers != 0
proc canCastle*(self: Chessboard, side: PieceColor): tuple[king, queen: bool] =
proc canCastle*(self: Chessboard): tuple[queen, king: bool] =
## Returns if the current side to move can castle
return (false, false) # TODO
if self.inCheck():
return (false, false)
let
sideToMove = self.position.sideToMove
occupancy = self.getOccupancy()
result = self.position.castlingAvailability[sideToMove.int]
if result.king:
result.king = (kingSideCastleRay(sideToMove) and occupancy) == 0
if result.queen:
result.queen = (queenSideCastleRay(sideToMove) and occupancy) == 0
if result.king:
# There are no pieces in between our friendly king and
# rook: check for attacks
let
king = self.getBitboard(King, sideToMove).toSquare()
for square in getRayBetween(king, sideToMove.kingSideRook()):
if self.isOccupancyAttacked(square, occupancy):
result.king = false
break
if result.queen:
let
king: Square = self.getBitboard(King, sideToMove).toSquare()
# The king always moves two squares, but the queen side rook moves
# 3 squares. We only need to check for attacks on the squares where
# the king moves to and not any further. We subtract 3 instead of 2
# because getRayBetween ignores the start and target squares in the
# ray it returns so we have to extend it by one
destination = makeSquare(rankFromSquare(king), fileFromSquare(king) - 3)
for square in getRayBetween(king, destination):
if self.isOccupancyAttacked(square, occupancy):
result.queen = false
break
proc update*(self: Chessboard) =
## Updates the internal grid representation
@ -475,8 +508,8 @@ proc toFEN*(self: Chessboard): string =
result &= (if self.position.sideToMove == White: "w" else: "b")
result &= " "
# Castling availability
let castleWhite = self.position.castlingAvailability.white
let castleBlack = self.position.castlingAvailability.black
let castleWhite = self.position.castlingAvailability[White.int]
let castleBlack = self.position.castlingAvailability[Black.int]
if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
result &= "-"
else:

View File

@ -38,7 +38,7 @@ type
## A magic bitboard entry
mask: Bitboard
value: uint64
indexBits: uint8
shift: uint8
# Yeah uh, don't look too closely at this...
@ -134,7 +134,7 @@ func getIndex*(magic: MagicEntry, blockers: Bitboard): uint {.inline.} =
let
blockers = blockers and magic.mask
hash = blockers * magic.value
index = hash shr (64'u8 - magic.indexBits)
index = hash shr magic.shift
return index.uint
@ -236,7 +236,7 @@ proc attemptMagicTableCreation(kind: PieceKind, square: Square, entry: MagicEntr
## (true, table) if successful, (false, empty) otherwise
# Initialize a new sequence with capacity 2^indexBits
result.table = newSeqOfCap[Bitboard](1 shl entry.indexBits)
result.table = newSeqOfCap[Bitboard](1 shl (64'u8 - entry.shift))
result.success = true
for _ in 0..result.table.capacity:
result.table.add(Bitboard(0))
@ -294,7 +294,7 @@ proc findMagic(kind: PieceKind, square: Square, indexBits: uint8): tuple[entry:
# hopefully better than a single one
let
magic = rand.next() and rand.next() and rand.next()
entry = MagicEntry(mask: mask, value: magic, indexBits: indexBits)
entry = MagicEntry(mask: mask, value: magic, shift: 64'u8 - indexBits)
var attempt = attemptMagicTableCreation(kind, square, entry)
if attempt.success:
# Huzzah! Our search for the mighty magic number is complete

View File

@ -14,7 +14,8 @@
## Move generation logic
import std/strformat
when not defined(danger):
import std/strformat
import bitboards
@ -37,9 +38,8 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
pawns = self.getBitboard(Pawn, sideToMove)
occupancy = self.getOccupancy()
# We can only capture enemy pieces (except the king)
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
epTarget = self.position.enPassantSquare
checkers = self.position.checkers
diagonalPins = self.position.diagonalPins
orthogonalPins = self.position.orthogonalPins
promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7)
@ -47,68 +47,104 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
# TODO: Give names to ranks and files so we don't have to assume a
# specific board layout when calling get(Rank|File)Mask
startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1)
var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0)
let epPawn = if epBitboard == 0: Bitboard(0) else: epBitboard.forwardRelativeTo(sideToMove)
# If we are in check, en passant is only possible if we'd capture the (only)
# checking pawn with it
if epBitboard != 0 and self.inCheck() and (epPawn and checkers).countSquares() == 0:
epBitboard = Bitboard(0)
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 diagonally, it cannot move
pushablePawns = pawns and not diagonalPins
# Neither can it move if it's pinned orthogonally
singlePushes = (pushablePawns.forwardRelativeTo(sideToMove) and not enemyPieces) and destinationMask and not orthogonalPins
# If a pawn is pinned horizontally, it cannot move either. It can move vertically
# though
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 and not orthogonalPins
canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy
canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy and destinationMask
for pawn in singlePushes:
for pawn in singlePushes and not orthogonalPins:
let pawnBB = pawn.toBitboard()
if promotionRank.contains(pawn):
for promotion in [PromoteToBishop, PromoteToBishop, PromoteToQueen, PromoteToRook]:
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn, promotion))
else:
moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn))
for pawn in canDoublePush:
for pawn in singlePushes and orthogonalPins:
let pawnBB = pawn.toBitboard()
if promotionRank.contains(pawn):
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn, promotion))
else:
moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn))
for pawn in canDoublePush and orthogonalPins:
moves.add(createMove(pawn.toBitboard().doubleBackwardRelativeTo(sideToMove), pawn, DoublePush))
for pawn in canDoublePush and not orthogonalPins:
moves.add(createMove(pawn.toBitboard().doubleBackwardRelativeTo(sideToMove), pawn, DoublePush))
let canCapture = pawns and not orthogonalPins
var
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))
# If a piece is pinned on the right, it can only capture on the right and
# vice versa for the left
if (let capture = diagonalPins and captureLeft; capture) != 0:
captureRight = Bitboard(0)
captureLeft = capture
if (let capture = diagonalPins and captureRight; capture) != 0:
captureLeft = Bitboard(0)
captureRight = capture
for pawn in captureRight:
let pawnBB = pawn.toBitboard()
if promotionRank.contains(pawn):
for promotion in [PromoteToBishop, PromoteToBishop, PromoteToQueen, PromoteToRook]:
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture, promotion))
else:
moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture))
for pawn in captureLeft:
let pawnBB = pawn.toBitboard()
if promotionRank.contains(pawn):
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture, promotion))
else:
moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture))
# 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
epPawn = epBitboard.backwardRelativeTo(sideToMove)
epLeft = pawns.forwardLeftRelativeTo(sideToMove) and epBitboard and destinationMask
epRight = pawns.forwardRightRelativeTo(sideToMove) and epBitboard and destinationMask
var
newOccupancy = occupancy and not epPawn
friendlyPawn: Bitboard = Bitboard(0)
if epLeft != 0:
friendlyPawn = epBitboard.backwardRightRelativeTo(sideToMove)
elif epRight != 0:
friendlyPawn = epBitboard.backwardLeftRelativeTo(sideToMove)
if friendlyPawn != 0:
# We basically simulate the en passant and see if the resulting
# occupancy bitboard has the king in check
newOccupancy = newOccupancy and not friendlyPawn
newOccupancy = newOccupancy or epBitboard
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
# En passant does not create a check on the king: all good
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
let
sideToMove = self.position.sideToMove
occupancy = self.getOccupancy()
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
rooks = self.getBitboard(Rook, sideToMove)
queens = self.getBitboard(Queen, sideToMove)
movableRooks = not self.position.diagonalPins and (queens or rooks)
@ -137,7 +173,7 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask:
let
sideToMove = self.position.sideToMove
occupancy = self.getOccupancy()
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
bishops = self.getBitboard(Bishop, sideToMove)
queens = self.getBitboard(Queen, sideToMove)
movableBishops = not self.position.orthogonalPins and (queens or bishops)
@ -168,7 +204,7 @@ proc generateKingMoves(self: Chessboard, moves: var MoveList) =
king = self.getBitboard(King, sideToMove)
occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite()
enemyPieces = self.getOccupancyFor(nonSideToMove)
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
bitboard = getKingAttacks(king.toSquare())
noKingOccupancy = occupancy and not king
for square in bitboard and not occupancy:
@ -186,20 +222,25 @@ proc generateKnightMoves(self: Chessboard, moves: var MoveList, destinationMask:
nonSideToMove = sideToMove.opposite()
pinned = self.position.diagonalPins or self.position.orthogonalPins
unpinnedKnights = knights and not pinned
enemyPieces = self.getOccupancyFor(nonSideToMove)
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 enemyPieces:
for target in bitboard and destinationMask 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
castlingRights = self.canCastle()
kingSquare = self.getBitboard(King, sideToMove).toSquare()
kingPiece = self.grid[kingSquare]
if castlingRights.king:
moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle))
if castlingRights.queen:
moves.add(createMove(kingSquare, kingPiece.queenSideCastling(), Castle))
proc generateMoves*(self: Chessboard, moves: var MoveList) =
@ -216,8 +257,8 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
# King is in double check: no need to generate any more
# moves
return
if not self.inCheck():
self.generateCastling(moves)
self.generateCastling(moves)
# We pass a mask to our move generators to remove stuff
# like our friendly pieces from the set of possible
@ -271,8 +312,8 @@ proc spawnPiece(self: Chessboard, square: Square, piece: Piece) =
proc removePiece(self: Chessboard, square: Square) =
## Removes a piece from the board, updating necessary
## metadata
var piece = self.grid[square]
when not defined(danger):
let Piece = self.grid[square]
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
self.removePieceFromBitboard(square)
self.grid[square] = nullPiece()
@ -291,6 +332,10 @@ proc movePiece(self: Chessboard, move: Move) =
self.spawnPiece(move.targetSquare, piece)
proc movePiece(self: Chessboard, startSquare, targetSquare: Square) =
self.movePiece(createMove(startSquare, targetSquare))
proc doMove*(self: Chessboard, move: Move) =
## Internal function called by makeMove after
## performing legality checks. Can be used in
@ -308,8 +353,8 @@ proc doMove*(self: Chessboard, move: Move) =
var
halfMoveClock = self.position.halfMoveClock
fullMoveCount = self.position.fullMoveCount
castlingRights = self.position.castlingRights
enPassantTarget = nullSquare()
# 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
@ -327,7 +372,6 @@ proc doMove*(self: Chessboard, move: Move) =
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
sideToMove: self.position.sideToMove.opposite(),
castlingRights: castlingRights,
enPassantSquare: enPassantTarget,
pieces: self.position.pieces,
castlingAvailability: self.position.castlingAvailability
@ -338,13 +382,39 @@ proc doMove*(self: Chessboard, move: Move) =
# Make the en passant pawn disappear
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare())
if move.isCastling() or piece.kind == King:
# If the king has moved, all castling rights for the side to
# move are revoked
self.position.castlingAvailability[piece.color.int] = (false, false)
if move.isCastling():
# Move the rook where it belongs
if move.targetSquare == piece.kingSideCastling():
let rook = self.grid[piece.color.kingSideRook()]
self.movePiece(piece.color.kingSideRook(), rook.kingSideCastling())
if move.targetSquare == piece.queenSideCastling():
let rook = self.grid[piece.color.queenSideRook()]
self.movePiece(piece.color.queenSideRook(), rook.queenSideCastling())
if piece.kind == Rook:
# If a rook on either side moves, castling rights are permanently revoked
# on that side
if move.startSquare == piece.color.kingSideRook():
self.position.castlingAvailability[piece.color.int].king = false
elif move.startSquare == piece.color.queenSideRook():
self.position.castlingAvailability[piece.color.int].queen = false
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare)
# If a rook has been captured, castling on that side is prohibited
if piece.kind == Rook:
if move.targetSquare == piece.color.kingSideRook():
self.position.castlingAvailability[piece.color.int].king = false
elif move.targetSquare == piece.color.queenSideRook():
self.position.castlingAvailability[piece.color.int].queen = false
# Move the piece to its target square
self.movePiece(move)
# TODO: Castling!
self.movePiece(move)
if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn
# and spawn a new piece
@ -361,7 +431,7 @@ proc doMove*(self: Chessboard, move: Move) =
else:
# Unreachable
discard
# Updates checks and pins for the side to move
# Updates checks and pins for the (new) side to move
self.updateChecksAndPins()

View File

@ -24,13 +24,12 @@ type
Capture = 2, # Move is a capture
DoublePush = 4, # Move is a double pawn push
# Castling metadata
CastleLong = 8,
CastleShort = 16,
Castle = 8,
# Pawn promotion metadata
PromoteToQueen = 32,
PromoteToRook = 64,
PromoteToBishop = 128,
PromoteToKnight = 256
PromoteToQueen = 16,
PromoteToRook = 32,
PromoteToBishop = 64,
PromoteToKnight = 128
Move* = object
## A chess move
@ -49,7 +48,6 @@ func `[]`*(self: MoveList, i: SomeInteger): Move =
raise newException(IndexDefect, &"move list access out of bounds ({i} >= {self.len})")
result = self.data[i]
iterator items*(self: MoveList): Move =
var i = 0
while self.len > i:
@ -63,6 +61,15 @@ iterator pairs*(self: MoveList): tuple[i: int, move: Move] =
yield (i, item)
func `$`*(self: MoveList): string =
result &= "["
for i, move in self:
result &= $move
if i < self.len:
result &= ", "
result &= "]"
func add*(self: var MoveList, move: Move) {.inline.} =
self.data[self.len] = move
inc(self.len)
@ -122,24 +129,13 @@ func getPromotionType*(move: Move): MoveFlag {.inline.} =
func isCapture*(move: Move): bool {.inline.} =
## Returns whether the given move is a
## cature
result = (move.flags and Capture.uint16) == Capture.uint16
result = (move.flags and Capture.uint16) != 0
func isCastling*(move: Move): bool {.inline.} =
## Returns whether the given move is a
## castle
for flag in [CastleLong, CastleShort]:
if (move.flags and flag.uint16) != 0:
return true
func getCastlingType*(move: Move): MoveFlag {.inline.} =
## Returns the castlingRights type of the given move.
## The return value of this function is only valid
## if isCastling() returns true
for flag in [CastleLong, CastleShort]:
if (move.flags and flag.uint16) != 0:
return flag
## castling move
result = (move.flags and Castle.uint16) != 0
func isEnPassant*(move: Move): bool {.inline.} =
@ -156,9 +152,9 @@ func isDoublePush*(move: Move): bool {.inline.} =
func getFlags*(move: Move): seq[MoveFlag] =
## Gets all the flags of this move
for flag in [EnPassant, Capture, DoublePush, CastleLong, CastleShort,
PromoteToBishop, PromoteToKnight, PromoteToQueen,
PromoteToRook]:
for flag in [EnPassant, Capture, DoublePush, Castle,
PromoteToBishop, PromoteToKnight, PromoteToQueen,
PromoteToRook]:
if (move.flags and flag.uint16) == flag.uint16:
result.add(flag)
if result.len() == 0:

View File

@ -93,11 +93,50 @@ proc toAlgebraic*(square: Square): string {.inline.} =
proc `$`*(square: Square): string = square.toAlgebraic()
func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare())
func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare())
func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare())
func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare())
func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare())
func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare())
func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a1".toSquare() else: "a8".toSquare())
func kingSideCastling*(piece: Piece): Square {.inline.} =
case piece.kind:
of Rook:
case piece.color:
of White:
return "f1".toSquare()
of Black:
return "f8".toSquare()
else:
discard
of King:
case piece.color:
of White:
return "g1".toSquare()
of Black:
return "g8".toSquare()
else:
discard
else:
discard
func queenSideCastling*(piece: Piece): Square {.inline.} =
case piece.kind:
of Rook:
case piece.color:
of White:
return "d1".toSquare()
of Black:
return "d8".toSquare()
else:
discard
of King:
case piece.color:
of White:
return "c1".toSquare()
of Black:
return "c8".toSquare()
else:
discard
else:
discard
proc toPretty*(piece: Piece): string =

View File

@ -26,7 +26,7 @@ type
# of whether the king or the rooks on either side
# moved, the actual checks for the legality of castling
# are done elsewhere
castlingAvailability*: tuple[white, black: tuple[queen, king: bool]]
castlingAvailability*: array[2, tuple[queen, king: bool]]
# Number of half-moves that were performed
# to reach this position starting from the
# root of the tree

View File

@ -14,6 +14,11 @@
import bitboards
import magics
import pieces
export bitboards, pieces
# Stolen from https://github.com/Ciekce/voidstar/blob/main/src/rays.rs :D
@ -46,3 +51,22 @@ let BETWEEN_RAYS = computeRaysBetweenSquares()
proc getRayBetween*(source, target: Square): Bitboard {.inline.} = BETWEEN_RAYS[source.int][target.int]
proc queenSideCastleRay*(color: PieceColor): Bitboard {.inline.} =
case color:
of White:
return getRayBetween("e1".toSquare(), "a1".toSquare())
of Black:
return getRayBetween("e8".toSquare(), "a8".toSquare())
else:
discard
proc kingSideCastleRay*(color: PieceColor): Bitboard {.inline.} =
case color:
of White:
return getRayBetween("e1".toSquare(), "h1".toSquare())
of Black:
return getRayBetween("e8".toSquare(), "h8".toSquare())
else:
discard

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -62,7 +62,7 @@ proc perft*(board: Chessboard, ply: int, verbose: bool = false, divide: bool = f
for move in moves:
if verbose:
let canCastle = board.canCastle(board.position.sideToMove)
let canCastle = board.canCastle()
echo &"Ply (from root): {board.position.plyFromRoot}"
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
echo &"Turn: {board.position.sideToMove}"
@ -91,7 +91,7 @@ proc perft*(board: Chessboard, ply: int, verbose: bool = false, divide: bool = f
# Opponent king is in check
inc(result.checks)
if verbose:
let canCastle = board.canCastle(board.position.sideToMove)
let canCastle = board.canCastle()
echo "\n"
echo &"Opponent in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
@ -239,11 +239,9 @@ proc handleMoveCommand(board: Chessboard, command: seq[string]): Move {.discarda
var move = createMove(startSquare, targetSquare, flags)
let piece = board.getPiece(move.startSquare)
if piece.kind == King and move.startSquare == board.position.sideToMove.getKingStartingSquare():
if move.targetSquare == longCastleKing(piece.color):
move.flags = move.flags or CastleLong.uint16
elif move.targetSquare == shortCastleKing(piece.color):
move.flags = move.flags or CastleShort.uint16
if move.targetSquare == board.position.enPassantSquare:
if move.targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]:
move.flags = move.flags or Castle.uint16
elif move.targetSquare == board.position.enPassantSquare:
move.flags = move.flags or EnPassant.uint16
result = board.makeMove(move)
if result == nullMove():
@ -258,8 +256,11 @@ proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
# some error occurs
var tempBoard: Chessboard
case command[1]:
of "startpos":
tempBoard = newDefaultChessboard()
of "startpos", "kiwipete":
if command[1] == "kiwipete":
tempBoard = newChessboardFromFen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -")
else:
tempBoard = newDefaultChessboard()
if command.len() > 2:
let args = command[2].splitWhitespace()
if args.len() > 0:
@ -337,18 +338,19 @@ const HELP_TEXT = """Nimfish help menu:
- fen [string]: Set the board to the given fen string if one is provided, or print
the current position as a FEN string if no arguments are given
- startpos: Set the board to the starting position
- kiwipete: Set the board to famous kiwipete position
- pretty: Pretty-print the current position
- print: Print the current position using ASCII characters only
Options:
- moves {moveList}: Perform the given moves (space-separated, all-lowercase)
in algebraic notation after the position is loaded. This option only applies
to the "startpos" and "fen" subcommands: it is ignored otherwise
to the subcommands that set a position, it is ignored otherwise
Examples:
- position startpos
- position fen "..." moves a2a3 a7a6
- clear: Clear the screen
- move <move>: Perform the given move in algebraic notation
- castle: Print castlingRights rights for each side
- castle: Print castling rights for the side to move
- check: Print if the current side to move is in check
- unmove, u: Unmakes the last move. Can be used in succession
- stm: Print which side is to move
@ -358,7 +360,8 @@ const HELP_TEXT = """Nimfish help menu:
- fen: Shorthand for "position fen"
- pos <args>: Shorthand for "position <args>"
- get <square>: Get the piece on the given square
- atk <square>: Print the attack bitboard of the given square for the side to move
- def <square>: Print the attack bitboard of the given square for the side to move
- atk <square>: Print the attack bitboard of the given square for the opponent side
- pins: Print the current pin mask
- checks: Print the current checks mask
- skip: Swap the side to move
@ -412,12 +415,21 @@ proc commandLoop*: int =
board.unmakeMove()
of "stm":
echo &"Side to move: {board.position.sideToMove}"
of "def":
if len(cmd) != 2:
echo "error: def: invalid number of arguments"
continue
try:
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove)
except ValueError:
echo "error: def: invalid square"
continue
of "atk":
if len(cmd) != 2:
echo "error: atk: invalid number of arguments"
continue
try:
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove)
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite())
except ValueError:
echo "error: atk: invalid square"
continue
@ -437,13 +449,15 @@ proc commandLoop*: int =
echo "error: get: invalid square"
continue
of "castle":
let canCastle = board.canCastle(board.position.sideToMove)
let canCastle = board.canCastle()
echo &"Castling rights for {($board.position.sideToMove).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
of "check":
echo &"{board.position.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
of "pins":
echo &"Ortogonal pins:\n{board.position.orthogonalPins}"
echo &"Diagonal pins:\n{board.position.diagonalPins}"
if board.position.orthogonalPins != 0:
echo &"Orthogonal pins:\n{board.position.orthogonalPins}"
if board.position.diagonalPins != 0:
echo &"Diagonal pins:\n{board.position.diagonalPins}"
of "checks":
echo board.position.checkers
of "quit":

View File

@ -19,7 +19,7 @@ def main(args: Namespace) -> int:
print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}")
return 2
try:
NIMFISH = (args.nimfish or (Path.cwd() / "bin" / "nimfish")).resolve(strict=True)
NIMFISH = (args.nimfish or Path(which("nimfish"))).resolve(strict=True)
except Exception as e:
print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}")
return 2
@ -166,4 +166,7 @@ if __name__ == "__main__":
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--silent", action="store_true", help="Disable all output (a return code of 0 means the test was successful)", default=False)
sys.exit(main(parser.parse_args()))
try:
sys.exit(main(parser.parse_args()))
except KeyboardInterrupt:
sys.exit(255)

View File

@ -1,3 +1,6 @@
1B6/8/8/8/5pP1/8/7k/4K3 b - g3 0 1
1k6/8/8/8/4Pp2/8/7B/4K3 b - e3 0 1
1k6/8/8/8/5pP1/8/7B/4K3 b - g3 0 1
1r2k2r/8/8/8/8/8/8/R3K2R b KQk - 0 1
1r2k2r/8/8/8/8/8/8/R3K2R w KQk - 0 1
2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1
@ -14,21 +17,56 @@
4k2r/8/8/8/8/8/8/4K3 b k - 0 1
4k2r/8/8/8/8/8/8/4K3 w k - 0 1
4k3/1P6/8/8/8/8/K7/8 w - - 0 1
4k3/2rn4/8/2K1pP2/8/8/8/8 w - e6 0 1
4k3/4p3/4K3/8/8/8/8/8 b - - 0 1
4k3/7K/8/5Pp1/8/8/8/1b6 w - g6 0 1
4k3/7b/8/4pP2/4K3/8/8/8 w - e6 0 1
4k3/7b/8/4pP2/8/8/8/1K6 w - e6 0 1
4k3/7b/8/5Pp1/8/8/8/1K6 w - g6 0 1
4k3/8/1b6/2Pp4/3K4/8/8/8 w - d6 0 1
4k3/8/3K4/1pP5/8/q7/8/8 w - b6 0 1
4k3/8/4q3/8/8/8/3b4/4K3 w - - 0 1
4k3/8/4r3/8/8/8/3p4/4K3 w - - 0 1
4k3/8/6b1/4pP2/4K3/8/8/8 w - e6 0 1
4k3/8/7b/5pP1/5K2/8/8/8 w - f6 0 1
4k3/8/8/2PpP3/8/8/8/4K3 w - d6 0 1
4k3/8/8/4pP2/3K4/8/8/8 w - e6 0 1
4k3/8/8/8/1b2r3/8/3Q4/4K3 w - - 0 1
4k3/8/8/8/1b2r3/8/3QP3/4K3 w - - 0 1
4k3/8/8/8/1b5b/2Q5/5P2/4K3 w - - 0 1
4k3/8/8/8/1b5b/2R5/5P2/4K3 w - - 0 1
4k3/8/8/8/1b5b/8/3Q4/4K3 w - - 0 1
4k3/8/8/8/1b5b/8/3R4/4K3 w - - 0 1
4k3/8/8/8/2pPp3/8/8/4K3 b - d3 0 1
4k3/8/8/8/8/8/8/4K2R b K - 0 1
4k3/8/8/8/8/8/8/4K2R w K - 0 1
4k3/8/8/8/8/8/8/R3K2R b KQ - 0 1
4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1
4k3/8/8/8/8/8/8/R3K3 b Q - 0 1
4k3/8/8/8/8/8/8/R3K3 w Q - 0 1
4k3/8/8/K2pP2q/8/8/8/8 w - d6 0 1
4k3/8/8/K2pP2r/8/8/8/8 w - d6 0 1
4k3/8/8/q2pP2K/8/8/8/8 w - d6 0 1
4k3/8/8/r2pP2K/8/8/8/8 w - d6 0 1
4k3/8/K6q/3pP3/8/8/8/8 w - d6 0 1
4k3/8/K6r/3pP3/8/8/8/8 w - d6 0 1
4k3/8/b7/1Pp5/2K5/8/8/8 w - c6 0 1
4k3/K7/8/1pP5/8/8/8/6b1 w - b6 0 1
4k3/b7/8/1pP5/8/8/8/6K1 w - b6 0 1
4k3/b7/8/2Pp4/3K4/8/8/8 w - d6 0 1
4k3/b7/8/2Pp4/8/8/8/6K1 w - d6 0 1
5k2/8/8/8/8/8/8/4K2R w K - 0 1
6B1/8/8/8/1Pp5/8/k7/4K3 b - b3 0 1
6KQ/8/8/8/8/8/8/7k b - - 0 1
6k1/8/8/8/1Pp5/8/B7/4K3 b - b3 0 1
6k1/8/8/8/2pP4/8/B7/3K4 b - d3 0 1
6kq/8/8/8/8/8/8/7K w - - 0 1
6qk/8/8/8/8/8/8/7K b - - 0 1
7k/3p4/8/8/3P4/8/8/K7 b - - 0 1
7k/3p4/8/8/3P4/8/8/K7 w - - 0 1
7K/7p/7k/8/8/8/8/8 b - - 0 1
7K/7p/7k/8/8/8/8/8 w - - 0 1
7k/3p4/8/8/3P4/8/8/K7 b - - 0 1
7k/3p4/8/8/3P4/8/8/K7 w - - 0 1
7k/4K3/8/1pP5/8/q7/8/8 w - b6 0 1
7k/8/1p6/8/8/P7/8/7K b - - 0 1
7k/8/1p6/8/8/P7/8/7K w - - 0 1
7k/8/8/1p6/P7/8/8/7K b - - 0 1
@ -52,8 +90,8 @@
8/3k4/3p4/8/3P4/3K4/8/8 w - - 0 1
8/8/1B6/7b/7k/8/2B1b3/7K b - - 0 1
8/8/1B6/7b/7k/8/2B1b3/7K w - - 0 1
8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1
8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1
8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1
8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1
8/8/3K4/3Nn3/3nN3/4k3/8/8 b - - 0 1
8/8/3k4/3p4/3P4/3K4/8/8 b - - 0 1
@ -65,6 +103,11 @@
8/8/7k/7p/7P/7K/8/8 b - - 0 1
8/8/7k/7p/7P/7K/8/8 w - - 0 1
8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3
8/8/8/4k3/5Pp1/8/8/3K4 b - f3 0 1
8/8/8/8/1R1Pp2k/8/8/4K3 b - d3 0 1
8/8/8/8/1k1Pp2R/8/8/4K3 b - d3 0 1
8/8/8/8/1k1PpN1R/8/8/4K3 b - d3 0 1
8/8/8/8/1k1Ppn1R/8/8/4K3 b - d3 0 1
8/8/8/8/8/4k3/4P3/4K3 w - - 0 1
8/8/8/8/8/7K/7P/7k b - - 0 1
8/8/8/8/8/7K/7P/7k w - - 0 1
@ -76,45 +119,49 @@
8/8/8/8/8/K7/P7/k7 w - - 0 1
8/8/k7/p7/P7/K7/8/8 b - - 0 1
8/8/k7/p7/P7/K7/8/8 w - - 0 1
8/k1P5/8/1K6/8/8/8/8 w - - 0 1
8/P1k5/K7/8/8/8/8/8 w - - 0 1
8/Pk6/8/8/8/8/6Kp/8 b - - 0 1
8/Pk6/8/8/8/8/6Kp/8 w - - 0 1
8/PPPk4/8/8/8/8/4Kppp/8 b - - 0 1
8/PPPk4/8/8/8/8/4Kppp/8 w - - 0 1
8/Pk6/8/8/8/8/6Kp/8 b - - 0 1
8/Pk6/8/8/8/8/6Kp/8 w - - 0 1
8/k1P5/8/1K6/8/8/8/8 w - - 0 1
B6b/8/8/8/2K5/4k3/8/b6B w - - 0 1
B6b/8/8/8/2K5/5k2/8/b6B b - - 0 1
K1k5/8/P7/8/8/8/8/8 w - - 0 1
K7/8/2n5/1n6/8/8/8/k6N b - - 0 1
K7/8/2n5/1n6/8/8/8/k6N w - - 0 1
K7/8/8/3Q4/4q3/8/8/7k b - - 0 1
K7/8/8/3Q4/4q3/8/8/7k w - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B b - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B w - - 0 1
K7/p7/k7/8/8/8/8/8 b - - 0 1
K7/p7/k7/8/8/8/8/8 w - - 0 1
R6r/8/8/2K5/5k2/8/8/r6R b - - 0 1
R6r/8/8/2K5/5k2/8/8/r6R w - - 0 1
k3K3/8/8/3pP3/8/8/8/4r3 w - d6 0 1
k7/6p1/8/8/8/8/7P/K7 b - - 0 1
k7/6p1/8/8/8/8/7P/K7 w - - 0 1
k7/7p/8/8/8/8/6P1/K7 b - - 0 1
k7/7p/8/8/8/8/6P1/K7 w - - 0 1
K7/8/2n5/1n6/8/8/8/k6N b - - 0 1
k7/8/2N5/1N6/8/8/8/K6n b - - 0 1
K7/8/2n5/1n6/8/8/8/k6N w - - 0 1
k7/8/2N5/1N6/8/8/8/K6n w - - 0 1
k7/8/3p4/8/3P4/8/8/7K b - - 0 1
k7/8/3p4/8/3P4/8/8/7K w - - 0 1
k7/8/3p4/8/8/4P3/8/7K b - - 0 1
k7/8/3p4/8/8/4P3/8/7K w - - 0 1
k7/8/4r3/3pP3/8/8/8/4K3 w - d6 0 1
k7/8/6p1/8/8/7P/8/K7 b - - 0 1
k7/8/6p1/8/8/7P/8/K7 w - - 0 1
k7/8/7p/8/8/6P1/8/K7 b - - 0 1
k7/8/7p/8/8/6P1/8/K7 w - - 0 1
k7/8/8/3p4/4p3/8/8/7K b - - 0 1
k7/8/8/3p4/4p3/8/8/7K w - - 0 1
K7/8/8/3Q4/4q3/8/8/7k b - - 0 1
K7/8/8/3Q4/4q3/8/8/7k w - - 0 1
k7/8/8/6p1/7P/8/8/K7 b - - 0 1
k7/8/8/6p1/7P/8/8/K7 w - - 0 1
k7/8/8/7p/6P1/8/8/K7 b - - 0 1
k7/8/8/7p/6P1/8/8/K7 w - - 0 1
k7/B7/1B6/1B6/8/8/8/K6b b - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B b - - 0 1
k7/B7/1B6/1B6/8/8/8/K6b w - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B w - - 0 1
K7/p7/k7/8/8/8/8/8 b - - 0 1
K7/p7/k7/8/8/8/8/8 w - - 0 1
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N b - - 0 1
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N w - - 0 1
n1n5/PPPk4/8/8/8/8/4Kppp/5N1N b - - 0 1
@ -142,8 +189,6 @@ r3k3/8/8/8/8/8/8/4K3 b q - 0 1
r3k3/8/8/8/8/8/8/4K3 w q - 0 1
r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10
r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2
R6r/8/8/2K5/5k2/8/8/r6R b - - 0 1
R6r/8/8/2K5/5k2/8/8/r6R w - - 0 1
rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9
rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8
rnbqkb1r/ppppp1pp/7n/4Pp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3

View File

@ -3,31 +3,62 @@ import timeit
from pathlib import Path
from argparse import Namespace, ArgumentParser
from compare_positions import main as test
from concurrent.futures import ThreadPoolExecutor, as_completed
from multiprocessing import cpu_count
from copy import deepcopy
def main(args: Namespace) -> int:
print("[S] Starting test suite")
# We try to be polite with resource usage
if not args.parallel:
print("[S] Starting test suite")
else:
print(f"[S] Starting test suite with {args.workers} workers")
successful = []
failed = []
positions = args.positions.read_text().splitlines()
start = timeit.default_timer()
longest_fen = max(sorted([len(fen) for fen in positions]))
for i, fen in enumerate(positions):
fen = fen.strip(" ")
fen += " " * (longest_fen - len(fen))
sys.stdout.write(f"\r[S] Testing {fen} ({i + 1}/{len(positions)})\033[K")
args.fen = fen
args.silent = not args.no_silent
if test(args) == 0:
successful.append(fen)
start = timeit.default_timer()
if not args.parallel:
for i, fen in enumerate(positions):
fen = fen.strip(" ")
fen += " " * (longest_fen - len(fen))
sys.stdout.write(f"\r[S] Testing {fen} ({i + 1}/{len(positions)})\033[K")
args.fen = fen
args.silent = not args.no_silent
if test(args) == 0:
successful.append(fen)
else:
failed.append(fen)
else:
# There is no compute going on in the Python thread,
# it's just I/O waiting for the processes to finish,
# so using a thread as opposed to a process doesn't
# make much different w.r.t. the GIL (and threads are
# cheaper than processes on some platforms)
futures = {}
try:
pool = ThreadPoolExecutor(args.workers)
for fen in positions:
args = deepcopy(args)
args.fen = fen.strip(" ")
args.silent = not args.no_silent
futures[pool.submit(test, args)] = args.fen
for i, future in enumerate(as_completed(futures)):
sys.stdout.write(f"\r[S] Testing in progress ({i + 1}/{len(positions)})\033[K")
if future.result() == 0:
successful.append(futures[future])
else:
failed.append(futures[future])
except KeyboardInterrupt:
pool.shutdown(cancel_futures=True)
print(f"\r[S] Interrupted\033[K")
else:
failed.append(fen)
stop = timeit.default_timer()
print(f"\r[S] Ran {len(positions)} tests at depth {args.ply} in {stop - start:.2f} seconds ({len(successful)} successful, {len(failed)} failed)\033[K")
if failed and args.show_failures:
print("[S] The following FENs failed to pass the test:", end="")
print("\n\t".join(failed))
stop = timeit.default_timer()
print(f"\r[S] Ran {len(positions)} tests at depth {args.ply} in {stop - start:.2f} seconds ({len(successful)} successful, {len(failed)} failed)\033[K")
if failed and args.show_failures:
print("[S] The following FENs failed to pass the test:", end="")
print("\n\t".join(failed))
if __name__ == "__main__":
@ -38,6 +69,11 @@ if __name__ == "__main__":
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--positions", type=Path, help="Location of the file containing FENs to test, one per line. Defaults to 'tests/positions.txt'",
default=Path("tests/positions.txt"))
parser.add_argument("--no-silent", action="store_true", help="Do not suppress output from compare_positions.py (defaults)", default=False)
parser.add_argument("--no-silent", action="store_true", help="Do not suppress output from compare_positions.py (defaults to False)", default=False)
parser.add_argument("-p", "--parallel", action="store_true", help="Run multiple tests in parallel", default=False)
parser.add_argument("--workers", "-w", type=int, required=False, help="How many workers to use in parallel mode (defaults to cpu_count() / 2)", default=cpu_count() // 2)
parser.add_argument("--show-failures", action="store_true", help="Show which FENs failed to pass the test", default=False)
sys.exit(main(parser.parse_args()))
try:
sys.exit(main(parser.parse_args()))
except KeyboardInterrupt:
sys.exit(255)