Pregenerate pawn attacks (currently unused). Attempts at fixing random crashing

This commit is contained in:
Mattia Giambirtone 2024-05-01 16:30:21 +02:00
parent c7ad3aa1b5
commit 70490f5698
8 changed files with 138 additions and 55 deletions

View File

@ -2,4 +2,8 @@
-o:"bin/nimfish"
-d:danger
--passL:"-flto"
--passC:"-Ofast -flto -march=native -mtune=native"
--passC:"-flto -march=native -mtune=native"
--mm:boehm
--stackTrace
--lineTrace
--debugger:native

View File

@ -302,10 +302,28 @@ func computeKnightBitboards: array[64, Bitboard] {.compileTime.} =
result[i] = movements
func computePawnAttacks(color: PieceColor): array[64, Bitboard] {.compileTime.} =
## Precomputes all the attack bitboards for pawns
## of the given color
for i in 0'u64..63:
let
pawn = i.toBitboard()
square = Square(i)
file = fileFromSquare(square)
var movements = Bitboard(0)
if file in 1..7:
movements = movements or pawn.forwardLeftRelativeTo(color)
if file in 0..6:
movements = movements or pawn.forwardRightRelativeTo(color)
movements = movements and not pawn
result[i] = movements
const
KING_BITBOARDS = computeKingBitboards()
KNIGHT_BITBOARDS = computeKnightBitboards()
PAWN_ATTACKS = [computePawnAttacks(White), computePawnAttacks(Black)]
func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square.int]
func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square.int]
func getPawnAttacks*(color: PieceColor, square: Square): Bitboard {.inline.} = PAWN_ATTACKS[color.int][square.int]

View File

@ -161,7 +161,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard =
# Backtrack so the space is seen by the
# next iteration of the loop
dec(index)
result.position.halfMoveClock = parseInt(s).int8
result.position.halfMoveClock = parseInt(s).uint16
of 5:
# Fullmove number
var s = ""
@ -603,7 +603,10 @@ proc drawByRepetition*(self: Chessboard): bool =
proc hash*(self: Chessboard) =
## Computes the zobrist hash of the current
## position
## position. This only needs to be called when
## a position is loaded the first time, as all
## subsequent hashes are updated incrementally
## at every call to doMove()
self.position.zobristKey = ZobristKey(0)
if self.position.sideToMove == Black:
@ -622,4 +625,4 @@ proc hash*(self: Chessboard) =
self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(Black)
if self.position.enPassantSquare != nullSquare():
self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(self.position.enPassantSquare))
self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(self.position.enPassantSquare))

View File

@ -349,12 +349,16 @@ proc doMove*(self: Chessboard, move: Move) =
fullMoveCount = self.position.fullMoveCount
enPassantTarget = nullSquare()
if self.position.enPassantSquare != nullSquare():
# Unset the previous en passant square in the zobrist key
self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(self.position.enPassantSquare))
# Needed to detect draw by the 50 move rule
if piece.kind == Pawn or move.isCapture() or move.isEnPassant():
# Number of half-moves since the last reversible half-move
halfMoveClock = 0
else:
inc(halfMoveClock)
if piece.color == Black:
inc(fullMoveCount)
@ -368,13 +372,18 @@ proc doMove*(self: Chessboard, move: Move) =
sideToMove: self.position.sideToMove.opposite(),
enPassantSquare: enPassantTarget,
pieces: self.position.pieces,
castlingAvailability: self.position.castlingAvailability
castlingAvailability: self.position.castlingAvailability,
zobristKey: self.position.zobristKey
)
if self.position.enPassantSquare != nullSquare():
self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(move.targetSquare))
# Update position metadata
if move.isEnPassant():
# Make the en passant pawn disappear
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare())
let epPawnSquare = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()
self.position.zobristKey = self.position.zobristKey xor self.getPiece(epPawnSquare).getKey(epPawnSquare)
self.removePiece(epPawnSquare)
if move.isCastling() or piece.kind == King:
# If the king has moved, all castling rights for the side to
@ -382,12 +391,24 @@ proc doMove*(self: Chessboard, move: Move) =
self.position.castlingAvailability[piece.color.int] = (false, false)
if move.isCastling():
# Move the rook where it belongs
var
source: Square
rook: Piece
target: Square
if move.targetSquare == piece.kingSideCastling():
let rook = self.getPiece(piece.color.kingSideRook())
self.movePiece(piece.color.kingSideRook(), rook.kingSideCastling())
if move.targetSquare == piece.queenSideCastling():
let rook = self.getPiece(piece.color.queenSideRook())
self.movePiece(piece.color.queenSideRook(), rook.queenSideCastling())
source = piece.color.kingSideRook()
rook = self.getPiece(source)
target = rook.kingSideCastling()
elif move.targetSquare == piece.queenSideCastling():
source = piece.color.queenSideRook()
rook = self.getPiece(source)
target = rook.queenSideCastling()
self.movePiece(source, target)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(source)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(target)
if piece.kind == Rook:
# If a rook on either side moves, castling rights are permanently revoked
@ -400,6 +421,7 @@ proc doMove*(self: Chessboard, move: Move) =
if move.isCapture():
# Get rid of captured pieces
let captured = self.getPiece(move.targetSquare)
self.position.zobristKey = self.position.zobristKey xor captured.getKey(move.targetSquare)
self.removePiece(move.targetSquare)
# If a rook has been captured, castling on that side is prohibited
if captured.kind == Rook:
@ -409,27 +431,36 @@ proc doMove*(self: Chessboard, move: Move) =
self.position.castlingAvailability[captured.color.int].queen = false
# Move the piece to its target square
self.movePiece(move)
self.movePiece(move)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.startSquare)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare)
if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn
# and spawn a new piece
self.removePiece(move.targetSquare)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare)
var spawnedPiece: Piece
case move.getPromotionType():
of PromoteToBishop:
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color))
spawnedPiece = Piece(kind: Bishop, color: piece.color)
of PromoteToKnight:
self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color))
spawnedPiece = Piece(kind: Knight, color: piece.color)
of PromoteToRook:
self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color))
spawnedPiece = Piece(kind: Rook, color: piece.color)
of PromoteToQueen:
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
spawnedPiece = Piece(kind: Queen, color: piece.color)
else:
# Unreachable
discard
self.position.zobristKey = self.position.zobristKey xor spawnedPiece.getKey(move.targetSquare)
self.spawnPiece(move.targetSquare, spawnedPiece)
# Updates checks and pins for the (new) side to move
self.updateChecksAndPins()
# Update zobrist key
self.hash()
# Last updates to zobrist key
if self.position.castlingAvailability[piece.color.int].king:
self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(piece.color)
if self.position.castlingAvailability[piece.color.int].queen:
self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(piece.color)
discard self.drawByRepetition()
@ -450,13 +481,11 @@ proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} =
proc unmakeMove*(self: Chessboard) =
## Reverts to the previous board position,
## if one exists
## Reverts to the previous board position
if self.positions.len() == 0:
return
self.position = self.positions.pop()
self.update()
self.hash()
## Testing stuff
@ -477,7 +506,7 @@ proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
if i != squares.len():
doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})"
## Tests
const testFens = staticRead("../../tests/all.txt").splitLines()

View File

@ -100,6 +100,7 @@ func createMove*(startSquare, targetSquare: Square, flags: varargs[MoveFlag]): M
for flag in flags:
result.flags = result.flags or flag.uint16
proc createMove*(startSquare, targetSquare: string, flags: varargs[MoveFlag]): Move =
result = createMove(startSquare.toSquare(), targetSquare.toSquare(), flags)
@ -113,6 +114,7 @@ func createMove*(startSquare: Square, targetSquare: SomeInteger, flags: varargs[
func nullMove*: Move {.inline.} = createMove(nullSquare(), nullSquare())
func isPromotion*(move: Move): bool {.inline.} =
## Returns whether the given move is a
## pawn promotion
@ -168,6 +170,8 @@ func getFlags*(move: Move): seq[MoveFlag] =
func `$`*(self: Move): string =
## Returns a string representation
## for the move
if self == nullMove():
return "null"
result &= &"{self.startSquare}{self.targetSquare}"
let flags = self.getFlags()
if len(flags) > 0:
@ -181,6 +185,8 @@ func `$`*(self: Move): string =
func toAlgebraic*(self: Move): string =
if self == nullMove():
return "null"
result &= &"{self.startSquare}{self.targetSquare}"
if self.isPromotion():
case self.getPromotionType():

View File

@ -18,8 +18,7 @@ import zobrist
type
Position* = ref object
Position* = object
## A chess position
# Castling availability. This just keeps track
@ -34,13 +33,12 @@ type
# Number of half moves since
# last piece capture or pawn movement.
# Used for the 50-move rule
halfMoveClock*: int8
halfMoveClock*: uint16
# Full move counter. Increments
# every 2 ply (half-moves)
fullMoveCount*: uint16
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
enPassantSquare*: Square
# The side to move
sideToMove*: PieceColor
# Positional bitboards for all pieces

View File

@ -30,7 +30,8 @@ type
SearchManager* = ref object
## A simple state storage
## for our search
stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread
searching: Atomic[bool]
stopFlag: Atomic[bool] # Can be used to cancel the search from another thread
board: Chessboard
bestMoveRoot: Move
bestRootScore: Score
@ -51,6 +52,19 @@ proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager
result.transpositionTable = transpositions
proc isSearching*(self: SearchManager): bool =
## Returns whether a search for the best
## move is in progress
result = self.searching.load()
proc stop*(self: SearchManager) =
## Stops the search if it is
## running
if self.isSearching():
self.stopFlag.store(true)
proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
## Returns an estimated static score for the move
result = Score(0)
@ -84,7 +98,7 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
if self.board.getPawnAttacks(move.targetSquare, nonSideToMove) != 0:
# Moving on a square attacked by an enemy pawn is _usually_ a very bad
# idea. Assume the piece is lost and give a malus based on the fact that
# losing a piece this way is a very poor move
# losing a piece this way is dumb
result -= self.board.getPieceScore(move.startSquare) * 2
@ -159,8 +173,9 @@ proc qsearch(self: SearchManager, ply: uint8, alpha, beta: Score): Score =
## or sacking pieces for apparently no reason: the reason is that it
## did not look at the opponent's responses, because it stopped earlier.
## That's the horizon). To address this, we look at all possible captures
## in the current position and make sure that a position is not evaluated as
## bad if only bad capture moves exist, if good non-capture moves do
## in the current position and make sure that a position is evaluated as
## bad if only bad capture moves are possible, even if good non-capture moves
## exist
if self.shouldStop():
return
if ply == 127:
@ -202,7 +217,10 @@ proc qsearch(self: SearchManager, ply: uint8, alpha, beta: Score): Score =
proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.discardable.} =
## Simple negamax search with alpha-beta pruning
if self.shouldStop():
if depth > 1 and self.shouldStop():
# We do not let ourselves get cancelled at depth
# one because then we wouldn't have a move to return.
# In practice this should not be a problem
return
when defined(useTT):
let query = self.transpositionTable.get(self.board.position.zobristKey, depth.uint8)
@ -219,8 +237,10 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
if depth == 0:
# Quiescent search gain: 264.8 +/- 71.6
return self.qsearch(0, alpha, beta)
var moves = newMoveList()
var depth = depth
var
moves = newMoveList()
depth = depth
bestMove = nullMove()
self.board.generateMoves(moves)
self.reorderMoves(moves)
if moves.len() == 0:
@ -265,7 +285,7 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
# When a search is cancelled or times out, we need
# to make sure the entire call stack unwinds back
# to the root move. This is why the check is duplicated
if self.shouldStop():
if depth > 1 and self.shouldStop():
return
bestScore = max(score, bestScore)
let nodeType = if score >= beta: LowerBound elif score <= alpha: UpperBound else: Exact
@ -277,6 +297,7 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
break
if score > alpha:
alpha = score
bestMove = move
if ply == 0:
self.bestMoveRoot = move
self.bestRootScore = bestScore
@ -291,7 +312,11 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes:
## is picked). If maxNodes is supplied and is nonzero,
## search will stop once it has analyzed the given number
## of nodes. If searchMoves is provided and is not empty,
## search will be restricted to the moves in the list
## search will be restricted to the moves in the list. Note
## that regardless of any time limitations, the search will
## not be cancelled until it has at least clear depth one
## (this is to make sure that there is always a best move to
## return)
self.bestMoveRoot = nullMove()
result = self.bestMoveRoot
self.maxNodes = maxNodes
@ -301,6 +326,7 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes:
var maxDepth = maxDepth
if maxDepth == -1:
maxDepth = 30
self.searching.store(true)
# Iterative deepening loop
for i in 1..maxDepth:
# Search the previous best move first
@ -316,4 +342,5 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes:
self.log(i - 1)
break
else:
self.log(i)
self.log(i)
self.searching.store(false)

View File

@ -18,6 +18,7 @@ import std/strformat
import std/atomics
import board
import movegen
import search
@ -28,8 +29,7 @@ type
UCISession = ref object
debug: bool
board: Chessboard
searching: bool
currentSearch: SearchManager
currentSearch: Atomic[SearchManager]
hashTableSize: uint64
transpositionTable: TTable
@ -292,38 +292,37 @@ proc parseUCICommand(session: UCISession, command: string): UCICommand =
proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} =
## Finds the best move in the current position
setControlCHook(proc () {.noconv.} = quit(0))
{.cast(gcsafe).}:
setControlCHook(proc () {.noconv.} = quit(0))
# Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread
var session = args.session
if session.transpositionTable.isNil():
if session.debug:
echo &"info string created {session.hashTableSize} MiB TT"
session.transpositionTable = newTranspositionTable(session.hashTableSize * 1024 * 1024)
when defined(useTT):
if session.transpositionTable.isNil():
if session.debug:
echo &"info string created {session.hashTableSize} MiB TT"
session.transpositionTable = newTranspositionTable(session.hashTableSize * 1024 * 1024)
var command = args.command
session.searching = true
session.currentSearch = newSearchManager(session.board, session.transpositionTable)
var searcher = newSearchManager(session.board.deepCopy(), session.transpositionTable)
session.currentSearch.store(searcher)
var
timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime)
increment = (if session.board.position.sideToMove == White: command.winc else: command.binc)
maxTime = (timeRemaining div 20) + (increment div 2)
if maxTime == 0:
maxTime = int32.high()
else:
# Buffer to avoid losing on time
maxTime -= 100
if command.moveTime != -1:
maxTime = command.moveTime
# Apparently negative remaining time is a thing. Welp
maxTime = max(1, maxTime)
if session.debug:
echo &"info string starting search to depth {command.depth} for at most {maxTime} ms and {command.nodes} nodes"
if session.debug and command.searchmoves.len() > 0:
echo &"""info string restricting search to: {command.searchmoves.join(" ")}"""
var move = session.currentSearch.findBestMove(maxTime, command.depth, command.nodes, command.searchmoves)
session.searching = false
var move = searcher.findBestMove(maxTime, command.depth, command.nodes, command.searchmoves)
echo &"bestmove {move.toAlgebraic()}"
proc startUCISession* =
## Begins listening for UCI commands
echo "id name Nimfish 0.1"
@ -361,8 +360,7 @@ proc startUCISession* =
var thread: Thread[tuple[session: UCISession, command: UCICommand]]
createThread(thread, bestMove, (session, cmd))
of Stop:
if session.searching:
session.currentSearch.stopFlag.store(true)
session.currentSearch.load().stop()
of SetOption:
case cmd.name:
of "Hash":