Bug fixes. Implement quiescent search, extensions and LMR. Needs SPRT
This commit is contained in:
parent
3f283932d8
commit
4381298957
|
@ -153,7 +153,11 @@ const
|
|||
-53, -34, -21, -11, -28, -14, -24, -43
|
||||
]
|
||||
|
||||
MIDDLEGAME_TABLES: array[6, array[64, Score]] = [
|
||||
# Bishop, King, Knight, Pawn, Queen, Rook
|
||||
MIDDLEGAME_WEIGHTS: array[6, Score] = [365, 0, 337, 82, 1025, 477]
|
||||
ENDGAME_WEIGHTS: array[6, Score] = [297, 0, 281, 94, 936, 512]
|
||||
|
||||
MIDDLEGAME_PSQ_TABLES: array[6, array[64, Score]] = [
|
||||
BISHOP_MIDDLEGAME_SCORES,
|
||||
KING_MIDDLEGAME_SCORES,
|
||||
KNIGHT_MIDDLEGAME_SCORES,
|
||||
|
@ -162,7 +166,7 @@ const
|
|||
ROOK_MIDDLEGAME_SCORES
|
||||
]
|
||||
|
||||
ENDGAME_TABLES: array[6, array[64, Score]] = [
|
||||
ENDGAME_PSQ_TABLES: array[6, array[64, Score]] = [
|
||||
BISHOP_ENDGAME_SCORES,
|
||||
KING_ENDGAME_SCORES,
|
||||
KNIGHT_ENDGAME_SCORES,
|
||||
|
@ -171,33 +175,31 @@ const
|
|||
ROOK_ENDGAME_SCORES
|
||||
]
|
||||
|
||||
|
||||
func lowestEval*: Score {.inline.} = Score(-20_000)
|
||||
func highestEval*: Score {.inline.} = Score(20_000)
|
||||
func mateScore*: Score {.inline.} = lowestEval()
|
||||
DOUBLED_PAWNS_MALUS: array[9, Score] = [0, -5, -10, -20, -30, -30, -30, -30, -30]
|
||||
ISOLATED_PAWN_MALUS: array[9, Score] = [0, -10, -25, -50, -75, -75, -75, -75, -75]
|
||||
|
||||
|
||||
proc getPieceValue(kind: PieceKind): Score =
|
||||
## Returns the absolute value of a piece
|
||||
case kind:
|
||||
of Pawn:
|
||||
return Score(100)
|
||||
of Bishop:
|
||||
return Score(330)
|
||||
of Knight:
|
||||
return Score(280)
|
||||
of Rook:
|
||||
return Score(525)
|
||||
of Queen:
|
||||
return Score(950)
|
||||
else:
|
||||
discard
|
||||
var
|
||||
MIDDLEGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]]
|
||||
ENDGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]]
|
||||
|
||||
|
||||
proc getPieceScore*(board: Chessboard, square: Square): Score =
|
||||
## Returns the value of the piece located at
|
||||
## the given square
|
||||
return board.getPiece(square).kind.getPieceValue()
|
||||
|
||||
proc initializeTables =
|
||||
for kind in [Bishop, King, Knight, Pawn, Queen, Rook]:
|
||||
for sq in 0..63:
|
||||
MIDDLEGAME_VALUE_TABLES[White.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq]
|
||||
ENDGAME_VALUE_TABLES[White.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq]
|
||||
MIDDLEGAME_VALUE_TABLES[Black.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq xor 56]
|
||||
ENDGAME_VALUE_TABLES[Black.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq xor 56]
|
||||
|
||||
|
||||
initializeTables()
|
||||
|
||||
|
||||
func lowestEval*: Score {.inline.} = Score(-25_000)
|
||||
func highestEval*: Score {.inline.} = Score(25_000)
|
||||
func mateScore*: Score {.inline.} = lowestEval() + 1
|
||||
|
||||
|
||||
proc getGamePhase(board: Chessboard): int =
|
||||
|
@ -219,9 +221,33 @@ proc getGamePhase(board: Chessboard): int =
|
|||
result = min(24, result)
|
||||
|
||||
|
||||
proc evaluatePiecePositions(board: ChessBoard): Score =
|
||||
## Returns the evaluation of the current
|
||||
## material's position relative to white
|
||||
proc getPieceScore*(board: Chessboard, square: Square): Score =
|
||||
## Returns the value of the piece located at
|
||||
## the given square
|
||||
let
|
||||
piece = board.getPiece(square)
|
||||
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
|
||||
endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
|
||||
middleGamePhase = board.getGamePhase()
|
||||
endGamePhase = 24 - middleGamePhase
|
||||
|
||||
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
|
||||
|
||||
|
||||
proc getPieceScore*(board: Chessboard, piece: Piece, square: Square): Score =
|
||||
## Returns the value the given piece would have if it
|
||||
## were at the given square
|
||||
let
|
||||
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
|
||||
endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
|
||||
middleGamePhase = board.getGamePhase()
|
||||
endGamePhase = 24 - middleGamePhase
|
||||
|
||||
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
|
||||
|
||||
proc evaluateMaterial(board: ChessBoard): Score =
|
||||
## Returns a material and position evaluation
|
||||
## for the current side to move
|
||||
let
|
||||
middleGamePhase = board.getGamePhase()
|
||||
endGamePhase = 24 - middleGamePhase
|
||||
|
@ -232,8 +258,8 @@ proc evaluatePiecePositions(board: ChessBoard): Score =
|
|||
|
||||
for sq in board.getOccupancy():
|
||||
let piece = board.getPiece(sq)
|
||||
middleGameScores[piece.color.int] += MIDDLEGAME_TABLES[piece.kind.int][sq.int]
|
||||
endGameScores[piece.color.int] += ENDGAME_TABLES[piece.kind.int][sq.int]
|
||||
middleGameScores[piece.color.int] += MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int]
|
||||
endGameScores[piece.color.int] += ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int]
|
||||
|
||||
let
|
||||
sideToMove = board.position.sideToMove
|
||||
|
@ -244,27 +270,35 @@ proc evaluatePiecePositions(board: ChessBoard): Score =
|
|||
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
|
||||
|
||||
|
||||
proc evaluateMaterial(board: ChessBoard): Score =
|
||||
## Returns the material evaluation of the
|
||||
## current position relative to white (positive
|
||||
## if in white's favor, negative otherwise)
|
||||
var
|
||||
whiteScore: Score
|
||||
blackScore: Score
|
||||
proc evaluatePawnStructure(board: Chessboard): Score =
|
||||
## Evaluates the pawn structure of the current
|
||||
## position for the side to move
|
||||
let
|
||||
sideToMove = board.position.sideToMove
|
||||
friendlyPawns = board.getOccupancyFor(sideToMove)
|
||||
|
||||
for sq in board.getOccupancyFor(White):
|
||||
whiteScore += board.getPieceScore(sq)
|
||||
|
||||
for sq in board.getOccupancyFor(Black):
|
||||
blackScore += board.getPieceScore(sq)
|
||||
|
||||
result = whiteScore - blackScore
|
||||
if board.position.sideToMove == Black:
|
||||
result *= -1
|
||||
# Doubled pawns are a bad idea
|
||||
var doubledPawns = 0
|
||||
for file in 0..7:
|
||||
if (getFileMask(file) and friendlyPawns).countSquares() > 1:
|
||||
inc(doubledPawns)
|
||||
|
||||
# Isolated pawns are also a bad idea
|
||||
var isolatedPawns = 0
|
||||
for pawn in friendlyPawns:
|
||||
let file = fileFromSquare(pawn)
|
||||
var fileMask = getFileMask(file)
|
||||
if file - 1 in 0..7:
|
||||
fileMask = fileMask or getFileMask(file - 1)
|
||||
if file + 1 in 0..7:
|
||||
fileMask = fileMask or getFileMask(file + 1)
|
||||
if (friendlyPawns and fileMask) == 0:
|
||||
inc(isolatedPawns)
|
||||
return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns]
|
||||
|
||||
|
||||
proc evaluate*(board: Chessboard): Score =
|
||||
## Evaluates the current position
|
||||
|
||||
result = board.evaluateMaterial()
|
||||
result += board.evaluatePiecePositions()
|
||||
result += board.evaluatePawnStructure()
|
|
@ -226,7 +226,7 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask:
|
|||
moves.add(createMove(square, target, Capture))
|
||||
|
||||
|
||||
proc generateKingMoves(self: Chessboard, moves: var MoveList) =
|
||||
proc generateKingMoves(self: Chessboard, moves: var MoveList, capturesOnly=false) =
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
king = self.getBitboard(King, sideToMove)
|
||||
|
@ -235,9 +235,10 @@ proc generateKingMoves(self: Chessboard, moves: var MoveList) =
|
|||
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
|
||||
bitboard = getKingAttacks(king.toSquare())
|
||||
noKingOccupancy = occupancy and not king
|
||||
for square in bitboard and not occupancy:
|
||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
||||
moves.add(createMove(king, square))
|
||||
if not capturesOnly:
|
||||
for square in bitboard and not occupancy:
|
||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
||||
moves.add(createMove(king, square))
|
||||
for square in bitboard and enemyPieces:
|
||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
||||
moves.add(createMove(king, square, Capture))
|
||||
|
@ -271,12 +272,15 @@ proc generateCastling(self: Chessboard, moves: var MoveList) =
|
|||
moves.add(createMove(kingSquare, kingPiece.queenSideCastling(), Castle))
|
||||
|
||||
|
||||
proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
||||
proc generateMoves*(self: Chessboard, moves: var MoveList, capturesOnly: bool = false) =
|
||||
## Generates the list of all possible legal moves
|
||||
## in the current position
|
||||
## in the current position. If capturesOnly is
|
||||
## true, only capture moves are generated
|
||||
|
||||
let sideToMove = self.position.sideToMove
|
||||
self.generateKingMoves(moves)
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
self.generateKingMoves(moves, capturesOnly)
|
||||
if self.position.checkers.countSquares() > 1:
|
||||
# King is in double check: no need to generate any more
|
||||
# moves
|
||||
|
@ -304,8 +308,8 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
|||
let
|
||||
checker = self.position.checkers.lowestSquare()
|
||||
checkerBB = checker.toBitboard()
|
||||
epTarget = self.position.enPassantSquare
|
||||
checkerPiece = self.getPiece(checker)
|
||||
# epTarget = self.position.enPassantSquare
|
||||
# checkerPiece = self.getPiece(checker)
|
||||
destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB
|
||||
# TODO: This doesn't really work. I've addressed the issue for now, but it's kinda ugly. Find a better
|
||||
# solution
|
||||
|
@ -315,6 +319,10 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
|||
# # for most pieces, because the move generators won't allow them to move there, but it does matter
|
||||
# # for pawns
|
||||
# destinationMask = destinationMask or epTarget.toBitboard()
|
||||
if capturesOnly:
|
||||
# Note: This does not cover en passant (which is good because it's a capture,
|
||||
# but the "fix" stands on flimsy ground)
|
||||
destinationMask = destinationMask and self.getOccupancyFor(nonSideToMove)
|
||||
self.generatePawnMoves(moves, destinationMask)
|
||||
self.generateKnightMoves(moves, destinationMask)
|
||||
self.generateRookMoves(moves, destinationMask)
|
||||
|
@ -427,7 +435,7 @@ proc doMove*(self: Chessboard, move: Move) =
|
|||
|
||||
proc isLegal*(self: Chessboard, move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is legal
|
||||
var moves = MoveList()
|
||||
var moves = newMoveList()
|
||||
self.generateMoves(moves)
|
||||
return move in moves
|
||||
|
||||
|
|
|
@ -193,4 +193,10 @@ func toAlgebraic*(self: Move): string =
|
|||
of PromoteToRook:
|
||||
result &= "r"
|
||||
else:
|
||||
discard
|
||||
discard
|
||||
|
||||
|
||||
proc newMoveList*: MoveList =
|
||||
new(result)
|
||||
for i in 0..result.data.high():
|
||||
result.data[i] = nullMove()
|
|
@ -30,7 +30,7 @@ type
|
|||
# Number of half-moves that were performed
|
||||
# to reach this position starting from the
|
||||
# root of the tree
|
||||
plyFromRoot*: int8
|
||||
plyFromRoot*: uint8
|
||||
# Number of half moves since
|
||||
# last piece capture or pawn movement.
|
||||
# Used for the 50-move rule
|
||||
|
|
|
@ -33,6 +33,7 @@ type
|
|||
stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread
|
||||
board: Chessboard
|
||||
bestMoveRoot: Move
|
||||
bestRootScore: Score
|
||||
searchStart: MonoTime
|
||||
searchDeadline: MonoTime
|
||||
nodeCount: uint64
|
||||
|
@ -40,6 +41,7 @@ type
|
|||
searchMoves: seq[Move]
|
||||
previousBestMove: Move
|
||||
transpositionTable: TTable
|
||||
currentExtensionCount: uint8
|
||||
|
||||
|
||||
proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager =
|
||||
|
@ -52,17 +54,37 @@ proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager
|
|||
proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
|
||||
## Returns an estimated static score for the move
|
||||
result = Score(0)
|
||||
let
|
||||
sideToMove = self.board.position.sideToMove
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
if self.previousBestMove != nullMove() and move == self.previousBestMove:
|
||||
result = highestEval() + 1
|
||||
elif move.isCapture():
|
||||
return highestEval() + 1
|
||||
if move.isCapture():
|
||||
# Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker
|
||||
# We prioritize moves that capture the most valuable pieces, and as a
|
||||
# second goal we want to use our least valuable pieces to do so (this
|
||||
# is why we multiply the score of the captured piece by 100, to give
|
||||
# it priority)
|
||||
result = 100 * self.board.getPieceScore(move.targetSquare) -
|
||||
self.board.getPieceScore(move.startSquare)
|
||||
|
||||
result += 100 * self.board.getPieceScore(move.targetSquare) - self.board.getPieceScore(move.startSquare)
|
||||
if move.isPromotion():
|
||||
# Promotions are a good idea to search first
|
||||
var piece: Piece
|
||||
case move.getPromotionType():
|
||||
of PromoteToBishop:
|
||||
piece = Piece(kind: Bishop, color: sideToMove)
|
||||
of PromoteToKnight:
|
||||
piece = Piece(kind: Knight, color: sideToMove)
|
||||
of PromoteToRook:
|
||||
piece = Piece(kind: Rook, color: sideToMove)
|
||||
of PromoteToQueen:
|
||||
piece = Piece(kind: Queen, color: sideToMove)
|
||||
else:
|
||||
discard # Unreachable
|
||||
result += self.board.getPieceScore(piece, move.targetSquare)
|
||||
if (self.board.getPawnAttacks(move.targetSquare, nonSideToMove) and self.board.getBitboard(Pawn, nonSideToMove)) != 0:
|
||||
# Moving on a square attacked by an enemy pawn is _usually_ a very bad
|
||||
# idea. Assume the piece is lost
|
||||
result -= self.board.getPieceScore(move.startSquare)
|
||||
|
||||
|
||||
proc reorderMoves(self: SearchManager, moves: var MoveList) =
|
||||
|
@ -71,8 +93,9 @@ proc reorderMoves(self: SearchManager, moves: var MoveList) =
|
|||
|
||||
proc orderer(a, b: Move): int {.closure.} =
|
||||
return cmp(self.getEstimatedMoveScore(a), self.getEstimatedMoveScore(b))
|
||||
|
||||
moves.data.sort(orderer, SortOrder.Descending)
|
||||
|
||||
# Ignore null moves beyond the lenght of the movelist
|
||||
sort(moves.data.toOpenArray(0, moves.len - 1), orderer, SortOrder.Descending)
|
||||
|
||||
|
||||
proc timedOut(self: SearchManager): bool = getMonoTime() >= self.searchDeadline
|
||||
|
@ -86,7 +109,7 @@ proc log(self: SearchManager, depth: int) =
|
|||
nps = 1000 * (self.nodeCount div max(elapsedMsec, 1))
|
||||
var logMsg = &"info depth {depth} time {elapsedMsec} nodes {self.nodeCount} nps {nps}"
|
||||
if self.bestMoveRoot != nullMove():
|
||||
logMsg &= &" pv {self.bestMoveRoot.toAlgebraic()}"
|
||||
logMsg &= &" bestmove {self.bestMoveRoot.toAlgebraic()} score {self.bestRootScore}"
|
||||
echo logMsg
|
||||
|
||||
|
||||
|
@ -104,7 +127,74 @@ proc shouldStop(self: SearchManager): bool =
|
|||
return true
|
||||
|
||||
|
||||
proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.discardable.} =
|
||||
proc getSearchExtension(self: SearchManager, move: Move): int =
|
||||
## Returns the number of extensions that should be performed
|
||||
## when exploring the given move
|
||||
if self.currentExtensionCount == 16:
|
||||
return 0
|
||||
if self.board.inCheck():
|
||||
# Opponent is in check: extend the search to see
|
||||
# if we can do other interesting things!
|
||||
inc(self.currentExtensionCount)
|
||||
return 1
|
||||
let piece = self.board.getPiece(move.targetSquare)
|
||||
# If a pawn has just moved to its second-last rank, extend to
|
||||
# see if a promotion would yield some good position
|
||||
if piece.kind == Pawn:
|
||||
let rank = if piece.color == White: getRankMask(1) else: getRankMask(6)
|
||||
if (move.targetSquare.toBitboard() and rank) != 0:
|
||||
inc(self.currentExtensionCount, 1)
|
||||
return 1
|
||||
|
||||
|
||||
proc qsearch(self: SearchManager, ply: uint8, alpha, beta: Score): Score =
|
||||
## Negamax search with a/b pruning that is restricted to
|
||||
## capture moves (commonly called quiescent search). The
|
||||
## purpose of this extra search step is to mitigate the
|
||||
## so called horizon effect that stems from the fact that,
|
||||
## at some point, the engine will have to stop searching, possibly
|
||||
## thinking a bad move is good because it couldn't see far enough
|
||||
## ahead (this usually results in the engine blundering captures
|
||||
## 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
|
||||
if self.shouldStop():
|
||||
return
|
||||
if ply == 127:
|
||||
return Score(0)
|
||||
let score = self.board.evaluate()
|
||||
if score >= beta:
|
||||
# Same as with the regular alpha-beta search
|
||||
return score
|
||||
var moves = newMoveList()
|
||||
self.board.generateMoves(moves, capturesOnly=true)
|
||||
self.reorderMoves(moves)
|
||||
var bestScore = score
|
||||
var alpha = max(alpha, score)
|
||||
for move in moves:
|
||||
self.board.doMove(move)
|
||||
inc(self.nodeCount)
|
||||
# Find the best move for us (worst move
|
||||
# for our opponent, hence the negative sign)
|
||||
var score = -self.qsearch(ply + 1, -beta, -alpha)
|
||||
self.board.unmakeMove()
|
||||
bestScore = max(score, bestScore)
|
||||
if score >= beta:
|
||||
# This move was too good for us, opponent will not search it
|
||||
break
|
||||
# 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():
|
||||
return
|
||||
if score > alpha:
|
||||
alpha = score
|
||||
return bestScore
|
||||
|
||||
|
||||
proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.discardable.} =
|
||||
## Simple negamax search with alpha-beta pruning
|
||||
if self.shouldStop():
|
||||
return
|
||||
|
@ -120,8 +210,8 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.
|
|||
if query.entry.score <= alpha:
|
||||
return query.entry.score
|
||||
if depth == 0:
|
||||
return self.board.evaluate()
|
||||
var moves = MoveList()
|
||||
return self.qsearch(0, alpha, beta)
|
||||
var moves = newMoveList()
|
||||
var depth = depth
|
||||
self.board.generateMoves(moves)
|
||||
self.reorderMoves(moves)
|
||||
|
@ -136,46 +226,49 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.
|
|||
# Stalemate
|
||||
return Score(0)
|
||||
var bestScore = lowestEval()
|
||||
var alpha = alpha
|
||||
var alpha = alpha
|
||||
for i, move in moves:
|
||||
if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves:
|
||||
continue
|
||||
self.board.doMove(move)
|
||||
var extension = self.getSearchExtension(move)
|
||||
let zobrist = self.board.position.zobristKey
|
||||
inc(self.nodeCount)
|
||||
# Find the best move for us (worst move
|
||||
# for our opponent, hence the negative sign)
|
||||
var score = -self.search(depth - 1, ply + 1, -beta, -alpha)
|
||||
if self.board.position.repetitionDraw:
|
||||
var score: Score
|
||||
var fullDepth = true
|
||||
if extension == 0 and i >= 3 and not move.isCapture():
|
||||
# Late Move Reduction: assume our move orderer did a good job,
|
||||
# so it is not worth to look at all moves at the same depth equally.
|
||||
# If this move turns out to be better than we expected, we'll re-search
|
||||
# it at full depth
|
||||
const reduction = 1
|
||||
score = -self.search(depth - 1 - reduction, ply + 1, -beta, -alpha)
|
||||
fullDepth = score > alpha
|
||||
if fullDepth:
|
||||
score = -self.search(depth - 1 + extension, ply + 1, -beta, -alpha)
|
||||
if self.board.position.halfMoveClock >= 100 or self.board.position.repetitionDraw:
|
||||
# Drawing by repetition is *bad*
|
||||
score = lowestEval() div 2
|
||||
score = Score(0)
|
||||
self.board.unmakeMove()
|
||||
# When a search is cancelled or times out, we need
|
||||
# to make sure the entire call stack unwindss back
|
||||
# to make sure the entire call stack unwinds back
|
||||
# to the root move. This is why the check is duplicated
|
||||
if self.shouldStop():
|
||||
return
|
||||
bestScore = max(score, bestScore)
|
||||
if score >= beta:
|
||||
# If we meet this position again, mark the fact that this score is a
|
||||
# lower bound for the actual true score of the node (i.e. its score
|
||||
# will NOT be lower than this)
|
||||
self.transpositionTable.store(depth.uint8, score, zobrist, LowerBound)
|
||||
let nodeType = if score >= beta: LowerBound elif score <= alpha: UpperBound else: Exact
|
||||
self.transpositionTable.store(depth.uint8, score, zobrist, nodeType)
|
||||
if nodeType == LowerBound:
|
||||
# score >= beta
|
||||
# This move was too good for us, opponent will not search it
|
||||
break
|
||||
if score <= alpha:
|
||||
# If we meet this position again, mark the fact that this score is an
|
||||
# upper bound for the actual true score of the node (i.e. its score
|
||||
# will NOT be higher than this)
|
||||
self.transpositionTable.store(depth.uint8, score, zobrist, UpperBound)
|
||||
else:
|
||||
# The position didn't cause any cutoffs, so the score stored here is
|
||||
# the actual true score of the position
|
||||
self.transpositionTable.store(depth.uint8, score, zobrist, Exact)
|
||||
if score > alpha:
|
||||
alpha = score
|
||||
if ply == 0:
|
||||
self.bestMoveRoot = move
|
||||
self.bestRootScore = bestScore
|
||||
return bestScore
|
||||
|
||||
|
||||
|
@ -202,16 +295,16 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes:
|
|||
# Search the previous best move first
|
||||
self.previousBestMove = self.bestMoveRoot
|
||||
self.search(i, 0, lowestEval(), highestEval())
|
||||
let shouldStop = self.shouldStop()
|
||||
if shouldStop:
|
||||
self.log(i - 1)
|
||||
else:
|
||||
self.log(i)
|
||||
# Since we always search the best move from the
|
||||
# previous iteration, we can use partial search
|
||||
# results: the engine will either not have changed
|
||||
# its mind, or it will have found an even better move
|
||||
# in the meantime, which we should obviously use!
|
||||
result = self.bestMoveRoot
|
||||
let shouldStop = self.shouldStop()
|
||||
if shouldStop:
|
||||
break
|
||||
self.log(i - 1)
|
||||
else:
|
||||
self.log(i)
|
||||
if shouldStop:
|
||||
break
|
|
@ -48,7 +48,8 @@ proc newTranspositionTable*(size: uint64): TTable =
|
|||
## Initializes a new transposition table of
|
||||
## size bytes
|
||||
new(result)
|
||||
result.data = newSeq[TTEntry](size)
|
||||
let numEntries = size div sizeof(TTEntry).uint64
|
||||
result.data = newSeq[TTEntry](numEntries)
|
||||
|
||||
|
||||
func getIndex(self: TTable, key: ZobristKey): uint64 =
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
# limitations under the License.
|
||||
|
||||
import movegen
|
||||
import eval
|
||||
import uci
|
||||
|
||||
|
||||
|
@ -28,12 +29,12 @@ type
|
|||
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
|
||||
|
||||
|
||||
proc perft*(board: Chessboard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
|
||||
proc perft*(board: Chessboard, ply: int, verbose = false, divide = false, bulk = false, capturesOnly = false): CountData =
|
||||
## Counts (and debugs) the number of legal positions reached after
|
||||
## the given number of ply
|
||||
|
||||
var moves = MoveList()
|
||||
board.generateMoves(moves)
|
||||
var moves = newMoveList()
|
||||
board.generateMoves(moves, capturesOnly=capturesOnly)
|
||||
if not bulk:
|
||||
if len(moves) == 0 and board.inCheck():
|
||||
result.checkmates = 1
|
||||
|
@ -146,6 +147,7 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) =
|
|||
args = command[2].splitWhitespace()
|
||||
bulk = false
|
||||
verbose = false
|
||||
captures = false
|
||||
if args.len() > 1:
|
||||
var ok = true
|
||||
for arg in args[1..^1]:
|
||||
|
@ -154,6 +156,8 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) =
|
|||
bulk = true
|
||||
of "verbose":
|
||||
verbose = true
|
||||
of "captures":
|
||||
captures = true
|
||||
else:
|
||||
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
||||
ok = false
|
||||
|
@ -164,13 +168,13 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) =
|
|||
let ply = parseInt(args[0])
|
||||
if bulk:
|
||||
let t = cpuTime()
|
||||
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
|
||||
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose, capturesOnly=captures).nodes
|
||||
let tot = cpuTime() - t
|
||||
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}"
|
||||
else:
|
||||
let t = cpuTime()
|
||||
let data = board.perft(ply, divide=true, verbose=verbose)
|
||||
let data = board.perft(ply, divide=true, verbose=verbose, capturesOnly=captures)
|
||||
let tot = cpuTime() - t
|
||||
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
||||
echo &" - Captures: {data.captures}"
|
||||
|
@ -327,6 +331,7 @@ const HELP_TEXT = """Nimfish help menu:
|
|||
Options:
|
||||
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
||||
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
||||
- captures: Only generate capture moves
|
||||
Example: go perft 5 bulk
|
||||
- position: Get/set board position
|
||||
Subcommands:
|
||||
|
@ -362,6 +367,8 @@ const HELP_TEXT = """Nimfish help menu:
|
|||
- uci: enter UCI mode
|
||||
- quit: exit
|
||||
- zobrist: Print the zobrist key for the current position
|
||||
- eval: Evaluate the current position
|
||||
- rep: Show whether this position is a draw by repetition
|
||||
"""
|
||||
|
||||
|
||||
|
@ -450,6 +457,8 @@ proc commandLoop*: int =
|
|||
echo board.position.zobristKey.uint64
|
||||
of "rep":
|
||||
echo board.position.repetitionDraw
|
||||
of "eval":
|
||||
echo &"Eval: {board.evaluate()}"
|
||||
else:
|
||||
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
||||
except IOError:
|
||||
|
|
Loading…
Reference in New Issue