Pregenerate pawn attacks (currently unused). Attempts at fixing random crashing
This commit is contained in:
parent
c7ad3aa1b5
commit
70490f5698
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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":
|
||||
|
|
Loading…
Reference in New Issue