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" -o:"bin/nimfish"
-d:danger -d:danger
--passL:"-flto" --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 '-': of '-':
discard discard
of 'K': of 'K':
result.position.castlingAvailability.white.king = true result.position.castlingAvailability[White.int].king = true
of 'Q': of 'Q':
result.position.castlingAvailability.white.queen = true result.position.castlingAvailability[White.int].queen = true
of 'k': of 'k':
result.position.castlingAvailability.black.king = true result.position.castlingAvailability[Black.int].king = true
of 'q': of 'q':
result.position.castlingAvailability.black.queen = true result.position.castlingAvailability[Black.int].queen = true
else: else:
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section") raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section")
of 3: of 3:
@ -230,7 +230,8 @@ func getKingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bi
result = Bitboard(0) result = Bitboard(0)
let let
king = self.getBitboard(King, attacker) king = self.getBitboard(King, attacker)
if (getKingAttacks(square) and king) != 0: squareBB = square.toBitboard()
if (getKingAttacks(square) and squareBB) != 0:
result = result or king 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 ## Returns the locations of the knights attacking the given square
let let
knights = self.getBitboard(Knight, attacker) knights = self.getBitboard(Knight, attacker)
squareBB = square.toBitboard()
result = Bitboard(0) result = Bitboard(0)
for knight in knights: for knight in knights:
let knightBB = knight.toBitboard() if (getKnightAttacks(knight) and squareBB) != 0:
if (getKnightAttacks(knight) and knightBB) != 0: result = result or knight.toBitboard()
result = result or knightBB
proc getSlidingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = proc getSlidingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
@ -369,10 +370,42 @@ func inCheck*(self: Chessboard): bool {.inline.} =
return self.position.checkers != 0 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 ## 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) = proc update*(self: Chessboard) =
## Updates the internal grid representation ## Updates the internal grid representation
@ -475,8 +508,8 @@ proc toFEN*(self: Chessboard): string =
result &= (if self.position.sideToMove == White: "w" else: "b") result &= (if self.position.sideToMove == White: "w" else: "b")
result &= " " result &= " "
# Castling availability # Castling availability
let castleWhite = self.position.castlingAvailability.white let castleWhite = self.position.castlingAvailability[White.int]
let castleBlack = self.position.castlingAvailability.black let castleBlack = self.position.castlingAvailability[Black.int]
if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
result &= "-" result &= "-"
else: else:

View File

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

View File

@ -14,7 +14,8 @@
## Move generation logic ## Move generation logic
import std/strformat when not defined(danger):
import std/strformat
import bitboards import bitboards
@ -37,9 +38,8 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
pawns = self.getBitboard(Pawn, sideToMove) pawns = self.getBitboard(Pawn, sideToMove)
occupancy = self.getOccupancy() occupancy = self.getOccupancy()
# We can only capture enemy pieces (except the king) # 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 epTarget = self.position.enPassantSquare
checkers = self.position.checkers
diagonalPins = self.position.diagonalPins diagonalPins = self.position.diagonalPins
orthogonalPins = self.position.orthogonalPins orthogonalPins = self.position.orthogonalPins
promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7) 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 # TODO: Give names to ranks and files so we don't have to assume a
# specific board layout when calling get(Rank|File)Mask # specific board layout when calling get(Rank|File)Mask
startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1) startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1)
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
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)
# Single and double pushes # Single and double pushes
# If a pawn is pinned diagonally, it cannot push forward
let let
# If a pawn is pinned diagonally, it cannot move # If a pawn is pinned horizontally, it cannot move either. It can move vertically
pushablePawns = pawns and not diagonalPins # though
# Neither can it move if it's pinned orthogonally horizontalPins = Bitboard((0xFF'u64 shl (rankFromSquare(friendlyKing).uint64 * 8))) and orthogonalPins
singlePushes = (pushablePawns.forwardRelativeTo(sideToMove) and not enemyPieces) and destinationMask and not 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 # 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 # way to check if there's pieces on the two squares ahead of the pawn
var canDoublePush = pushablePawns and startingRank 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 canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy and destinationMask
for pawn in singlePushes: for pawn in singlePushes and not orthogonalPins:
let pawnBB = pawn.toBitboard() let pawnBB = pawn.toBitboard()
if promotionRank.contains(pawn): 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)) moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn, promotion))
else: else:
moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn)) 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)) moves.add(createMove(pawn.toBitboard().doubleBackwardRelativeTo(sideToMove), pawn, DoublePush))
let canCapture = pawns and not orthogonalPins let canCapture = pawns and not orthogonalPins
var var
captureLeft = canCapture.forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask captureLeft = canCapture.forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask
captureRight = canCapture.forwardRightRelativeTo(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 a piece is pinned on the right, it can only capture on the right and
if (diagonalPins and captureLeft) != 0: # vice versa for the left
captureLeft = captureLeft and diagonalPins if (let capture = diagonalPins and captureLeft; capture) != 0:
if (diagonalPins and captureRight) != 0: captureRight = Bitboard(0)
captureRight = captureRight and diagonalPins captureLeft = capture
for pawn in captureLeft: if (let capture = diagonalPins and captureRight; capture) != 0:
let pawnBB = pawn.toBitboard() captureLeft = Bitboard(0)
if promotionRank.contains(pawn): captureRight = capture
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: for pawn in captureRight:
let pawnBB = pawn.toBitboard() let pawnBB = pawn.toBitboard()
if promotionRank.contains(pawn): 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)) moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture, promotion))
else: else:
moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture)) 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) = proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
let let
sideToMove = self.position.sideToMove sideToMove = self.position.sideToMove
occupancy = self.getOccupancy() 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) rooks = self.getBitboard(Rook, sideToMove)
queens = self.getBitboard(Queen, sideToMove) queens = self.getBitboard(Queen, sideToMove)
movableRooks = not self.position.diagonalPins and (queens or rooks) movableRooks = not self.position.diagonalPins and (queens or rooks)
@ -137,7 +173,7 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask:
let let
sideToMove = self.position.sideToMove sideToMove = self.position.sideToMove
occupancy = self.getOccupancy() 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) bishops = self.getBitboard(Bishop, sideToMove)
queens = self.getBitboard(Queen, sideToMove) queens = self.getBitboard(Queen, sideToMove)
movableBishops = not self.position.orthogonalPins and (queens or bishops) 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) king = self.getBitboard(King, sideToMove)
occupancy = self.getOccupancy() occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite() nonSideToMove = sideToMove.opposite()
enemyPieces = self.getOccupancyFor(nonSideToMove) enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
bitboard = getKingAttacks(king.toSquare()) bitboard = getKingAttacks(king.toSquare())
noKingOccupancy = occupancy and not king noKingOccupancy = occupancy and not king
for square in bitboard and not occupancy: for square in bitboard and not occupancy:
@ -186,20 +222,25 @@ proc generateKnightMoves(self: Chessboard, moves: var MoveList, destinationMask:
nonSideToMove = sideToMove.opposite() nonSideToMove = sideToMove.opposite()
pinned = self.position.diagonalPins or self.position.orthogonalPins pinned = self.position.diagonalPins or self.position.orthogonalPins
unpinnedKnights = knights and not pinned unpinnedKnights = knights and not pinned
enemyPieces = self.getOccupancyFor(nonSideToMove) enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
for square in unpinnedKnights: for square in unpinnedKnights:
let bitboard = getKnightAttacks(square) let bitboard = getKnightAttacks(square)
for target in bitboard and destinationMask and not enemyPieces: for target in bitboard and destinationMask and not enemyPieces:
moves.add(createMove(square, target)) 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)) moves.add(createMove(square, target, Capture))
proc generateCastling(self: Chessboard, moves: var MoveList) = proc generateCastling(self: Chessboard, moves: var MoveList) =
let let
sideToMove = self.position.sideToMove sideToMove = self.position.sideToMove
rooks = self.getBitboard(Rook, sideToMove) castlingRights = self.canCastle()
# TODO 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) = 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 # King is in double check: no need to generate any more
# moves # moves
return return
if not self.inCheck():
self.generateCastling(moves) self.generateCastling(moves)
# We pass a mask to our move generators to remove stuff # We pass a mask to our move generators to remove stuff
# like our friendly pieces from the set of possible # 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) = proc removePiece(self: Chessboard, square: Square) =
## Removes a piece from the board, updating necessary ## Removes a piece from the board, updating necessary
## metadata ## metadata
var piece = self.grid[square]
when not defined(danger): when not defined(danger):
let Piece = self.grid[square]
doAssert piece.kind != Empty and piece.color != None, self.toFEN() doAssert piece.kind != Empty and piece.color != None, self.toFEN()
self.removePieceFromBitboard(square) self.removePieceFromBitboard(square)
self.grid[square] = nullPiece() self.grid[square] = nullPiece()
@ -291,6 +332,10 @@ proc movePiece(self: Chessboard, move: Move) =
self.spawnPiece(move.targetSquare, piece) self.spawnPiece(move.targetSquare, piece)
proc movePiece(self: Chessboard, startSquare, targetSquare: Square) =
self.movePiece(createMove(startSquare, targetSquare))
proc doMove*(self: Chessboard, move: Move) = proc doMove*(self: Chessboard, move: Move) =
## Internal function called by makeMove after ## Internal function called by makeMove after
## performing legality checks. Can be used in ## performing legality checks. Can be used in
@ -308,8 +353,8 @@ proc doMove*(self: Chessboard, move: Move) =
var var
halfMoveClock = self.position.halfMoveClock halfMoveClock = self.position.halfMoveClock
fullMoveCount = self.position.fullMoveCount fullMoveCount = self.position.fullMoveCount
castlingRights = self.position.castlingRights
enPassantTarget = nullSquare() enPassantTarget = nullSquare()
# Needed to detect draw by the 50 move rule # Needed to detect draw by the 50 move rule
if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): if piece.kind == Pawn or move.isCapture() or move.isEnPassant():
# Number of half-moves since the last reversible half-move # Number of half-moves since the last reversible half-move
@ -327,7 +372,6 @@ proc doMove*(self: Chessboard, move: Move) =
halfMoveClock: halfMoveClock, halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount, fullMoveCount: fullMoveCount,
sideToMove: self.position.sideToMove.opposite(), sideToMove: self.position.sideToMove.opposite(),
castlingRights: castlingRights,
enPassantSquare: enPassantTarget, enPassantSquare: enPassantTarget,
pieces: self.position.pieces, pieces: self.position.pieces,
castlingAvailability: self.position.castlingAvailability castlingAvailability: self.position.castlingAvailability
@ -338,13 +382,39 @@ proc doMove*(self: Chessboard, move: Move) =
# Make the en passant pawn disappear # Make the en passant pawn disappear
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()) 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(): if move.isCapture():
# Get rid of captured pieces # Get rid of captured pieces
self.removePiece(move.targetSquare) 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 # Move the piece to its target square
self.movePiece(move) self.movePiece(move)
# TODO: Castling!
if move.isPromotion(): if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn # Move is a pawn promotion: get rid of the pawn
# and spawn a new piece # and spawn a new piece
@ -361,7 +431,7 @@ proc doMove*(self: Chessboard, move: Move) =
else: else:
# Unreachable # Unreachable
discard discard
# Updates checks and pins for the side to move # Updates checks and pins for the (new) side to move
self.updateChecksAndPins() self.updateChecksAndPins()

View File

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

View File

@ -93,11 +93,50 @@ proc toAlgebraic*(square: Square): string {.inline.} =
proc `$`*(square: Square): string = square.toAlgebraic() proc `$`*(square: Square): string = square.toAlgebraic()
func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) 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 queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a1".toSquare() else: "a8".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 kingSideCastling*(piece: Piece): Square {.inline.} =
func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare()) case piece.kind:
func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare()) 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 = proc toPretty*(piece: Piece): string =

View File

@ -26,7 +26,7 @@ type
# of whether the king or the rooks on either side # of whether the king or the rooks on either side
# moved, the actual checks for the legality of castling # moved, the actual checks for the legality of castling
# are done elsewhere # 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 # Number of half-moves that were performed
# to reach this position starting from the # to reach this position starting from the
# root of the tree # root of the tree

View File

@ -14,6 +14,11 @@
import bitboards import bitboards
import magics import magics
import pieces
export bitboards, pieces
# Stolen from https://github.com/Ciekce/voidstar/blob/main/src/rays.rs :D # 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 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: for move in moves:
if verbose: if verbose:
let canCastle = board.canCastle(board.position.sideToMove) let canCastle = board.canCastle()
echo &"Ply (from root): {board.position.plyFromRoot}" echo &"Ply (from root): {board.position.plyFromRoot}"
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}" echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
echo &"Turn: {board.position.sideToMove}" 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 # Opponent king is in check
inc(result.checks) inc(result.checks)
if verbose: if verbose:
let canCastle = board.canCastle(board.position.sideToMove) let canCastle = board.canCastle()
echo "\n" echo "\n"
echo &"Opponent in check: {(if board.inCheck(): \"yes\" else: \"no\")}" 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\")}" 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) var move = createMove(startSquare, targetSquare, flags)
let piece = board.getPiece(move.startSquare) let piece = board.getPiece(move.startSquare)
if piece.kind == King and move.startSquare == board.position.sideToMove.getKingStartingSquare(): if piece.kind == King and move.startSquare == board.position.sideToMove.getKingStartingSquare():
if move.targetSquare == longCastleKing(piece.color): if move.targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]:
move.flags = move.flags or CastleLong.uint16 move.flags = move.flags or Castle.uint16
elif move.targetSquare == shortCastleKing(piece.color): elif move.targetSquare == board.position.enPassantSquare:
move.flags = move.flags or CastleShort.uint16
if move.targetSquare == board.position.enPassantSquare:
move.flags = move.flags or EnPassant.uint16 move.flags = move.flags or EnPassant.uint16
result = board.makeMove(move) result = board.makeMove(move)
if result == nullMove(): if result == nullMove():
@ -258,8 +256,11 @@ proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
# some error occurs # some error occurs
var tempBoard: Chessboard var tempBoard: Chessboard
case command[1]: case command[1]:
of "startpos": of "startpos", "kiwipete":
tempBoard = newDefaultChessboard() if command[1] == "kiwipete":
tempBoard = newChessboardFromFen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -")
else:
tempBoard = newDefaultChessboard()
if command.len() > 2: if command.len() > 2:
let args = command[2].splitWhitespace() let args = command[2].splitWhitespace()
if args.len() > 0: 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 - 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 the current position as a FEN string if no arguments are given
- startpos: Set the board to the starting position - startpos: Set the board to the starting position
- kiwipete: Set the board to famous kiwipete position
- pretty: Pretty-print the current position - pretty: Pretty-print the current position
- print: Print the current position using ASCII characters only - print: Print the current position using ASCII characters only
Options: Options:
- moves {moveList}: Perform the given moves (space-separated, all-lowercase) - moves {moveList}: Perform the given moves (space-separated, all-lowercase)
in algebraic notation after the position is loaded. This option only applies 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: Examples:
- position startpos - position startpos
- position fen "..." moves a2a3 a7a6 - position fen "..." moves a2a3 a7a6
- clear: Clear the screen - clear: Clear the screen
- move <move>: Perform the given move in algebraic notation - 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 - check: Print if the current side to move is in check
- unmove, u: Unmakes the last move. Can be used in succession - unmove, u: Unmakes the last move. Can be used in succession
- stm: Print which side is to move - stm: Print which side is to move
@ -358,7 +360,8 @@ const HELP_TEXT = """Nimfish help menu:
- fen: Shorthand for "position fen" - fen: Shorthand for "position fen"
- pos <args>: Shorthand for "position <args>" - pos <args>: Shorthand for "position <args>"
- get <square>: Get the piece on the given square - 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 - pins: Print the current pin mask
- checks: Print the current checks mask - checks: Print the current checks mask
- skip: Swap the side to move - skip: Swap the side to move
@ -412,12 +415,21 @@ proc commandLoop*: int =
board.unmakeMove() board.unmakeMove()
of "stm": of "stm":
echo &"Side to move: {board.position.sideToMove}" 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": of "atk":
if len(cmd) != 2: if len(cmd) != 2:
echo "error: atk: invalid number of arguments" echo "error: atk: invalid number of arguments"
continue continue
try: try:
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove) echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite())
except ValueError: except ValueError:
echo "error: atk: invalid square" echo "error: atk: invalid square"
continue continue
@ -437,13 +449,15 @@ proc commandLoop*: int =
echo "error: get: invalid square" echo "error: get: invalid square"
continue continue
of "castle": 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\")}" 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": of "check":
echo &"{board.position.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}" echo &"{board.position.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
of "pins": of "pins":
echo &"Ortogonal pins:\n{board.position.orthogonalPins}" if board.position.orthogonalPins != 0:
echo &"Diagonal pins:\n{board.position.diagonalPins}" echo &"Orthogonal pins:\n{board.position.orthogonalPins}"
if board.position.diagonalPins != 0:
echo &"Diagonal pins:\n{board.position.diagonalPins}"
of "checks": of "checks":
echo board.position.checkers echo board.position.checkers
of "quit": of "quit":

View File

@ -19,7 +19,7 @@ def main(args: Namespace) -> int:
print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}") print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}")
return 2 return 2
try: 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: except Exception as e:
print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}") print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}")
return 2 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("--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("--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) 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 b KQk - 0 1
1r2k2r/8/8/8/8/8/8/R3K2R w 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 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 b k - 0 1
4k2r/8/8/8/8/8/8/4K3 w 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/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/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 b K - 0 1
4k3/8/8/8/8/8/8/4K2R w 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 b KQ - 0 1
4k3/8/8/8/8/8/8/R3K2R w 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 b Q - 0 1
4k3/8/8/8/8/8/8/R3K3 w 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 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 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 6kq/8/8/8/8/8/8/7K w - - 0 1
6qk/8/8/8/8/8/8/7K b - - 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 b - - 0 1
7K/7p/7k/8/8/8/8/8 w - - 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 b - - 0 1
7k/8/1p6/8/8/P7/8/7K w - - 0 1 7k/8/1p6/8/8/P7/8/7K w - - 0 1
7k/8/8/1p6/P7/8/8/7K b - - 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/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 b - - 0 1
8/8/1B6/7b/7k/8/2B1b3/7K w - - 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/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/2k5/5q2/5n2/8/5K2/8 b - - 0 1
8/8/3K4/3Nn3/3nN3/4k3/8/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 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 b - - 0 1
8/8/7k/7p/7P/7K/8/8 w - - 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/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/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 b - - 0 1
8/8/8/8/8/7K/7P/7k w - - 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/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 b - - 0 1
8/8/k7/p7/P7/K7/8/8 w - - 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/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 b - - 0 1
8/PPPk4/8/8/8/8/4Kppp/8 w - - 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/4k3/8/b6B w - - 0 1
B6b/8/8/8/2K5/5k2/8/b6B b - - 0 1 B6b/8/8/8/2K5/5k2/8/b6B b - - 0 1
K1k5/8/P7/8/8/8/8/8 w - - 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 b - - 0 1
k7/6p1/8/8/8/8/7P/K7 w - - 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 b - - 0 1
k7/7p/8/8/8/8/6P1/K7 w - - 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 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/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 b - - 0 1
k7/8/3p4/8/3P4/8/8/7K w - - 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 b - - 0 1
k7/8/3p4/8/8/4P3/8/7K w - - 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 b - - 0 1
k7/8/6p1/8/8/7P/8/K7 w - - 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 b - - 0 1
k7/8/7p/8/8/6P1/8/K7 w - - 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 b - - 0 1
k7/8/8/3p4/4p3/8/8/7K w - - 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 b - - 0 1
k7/8/8/6p1/7P/8/8/K7 w - - 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 b - - 0 1
k7/8/8/7p/6P1/8/8/K7 w - - 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 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/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 b - - 0 1
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N w - - 0 1 n1n5/1Pk5/8/8/8/8/5Kp1/5N1N w - - 0 1
n1n5/PPPk4/8/8/8/8/4Kppp/5N1N b - - 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 r3k3/8/8/8/8/8/8/4K3 w q - 0 1
r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10 r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10
r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2 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 rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9
rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8 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 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 pathlib import Path
from argparse import Namespace, ArgumentParser from argparse import Namespace, ArgumentParser
from compare_positions import main as test 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: 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 = [] successful = []
failed = [] failed = []
positions = args.positions.read_text().splitlines() positions = args.positions.read_text().splitlines()
start = timeit.default_timer()
longest_fen = max(sorted([len(fen) for fen in positions])) longest_fen = max(sorted([len(fen) for fen in positions]))
for i, fen in enumerate(positions): start = timeit.default_timer()
fen = fen.strip(" ") if not args.parallel:
fen += " " * (longest_fen - len(fen)) for i, fen in enumerate(positions):
sys.stdout.write(f"\r[S] Testing {fen} ({i + 1}/{len(positions)})\033[K") fen = fen.strip(" ")
args.fen = fen fen += " " * (longest_fen - len(fen))
args.silent = not args.no_silent sys.stdout.write(f"\r[S] Testing {fen} ({i + 1}/{len(positions)})\033[K")
if test(args) == 0: args.fen = fen
successful.append(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: else:
failed.append(fen) stop = timeit.default_timer()
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")
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:
if failed and args.show_failures: print("[S] The following FENs failed to pass the test:", end="")
print("[S] The following FENs failed to pass the test:", end="") print("\n\t".join(failed))
print("\n\t".join(failed))
if __name__ == "__main__": 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("--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'", 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")) 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) 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)