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
|
-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,
|
BISHOP_MIDDLEGAME_SCORES,
|
||||||
KING_MIDDLEGAME_SCORES,
|
KING_MIDDLEGAME_SCORES,
|
||||||
KNIGHT_MIDDLEGAME_SCORES,
|
KNIGHT_MIDDLEGAME_SCORES,
|
||||||
|
@ -162,7 +166,7 @@ const
|
||||||
ROOK_MIDDLEGAME_SCORES
|
ROOK_MIDDLEGAME_SCORES
|
||||||
]
|
]
|
||||||
|
|
||||||
ENDGAME_TABLES: array[6, array[64, Score]] = [
|
ENDGAME_PSQ_TABLES: array[6, array[64, Score]] = [
|
||||||
BISHOP_ENDGAME_SCORES,
|
BISHOP_ENDGAME_SCORES,
|
||||||
KING_ENDGAME_SCORES,
|
KING_ENDGAME_SCORES,
|
||||||
KNIGHT_ENDGAME_SCORES,
|
KNIGHT_ENDGAME_SCORES,
|
||||||
|
@ -171,33 +175,31 @@ const
|
||||||
ROOK_ENDGAME_SCORES
|
ROOK_ENDGAME_SCORES
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DOUBLED_PAWNS_MALUS: array[9, Score] = [0, -5, -10, -20, -30, -30, -30, -30, -30]
|
||||||
func lowestEval*: Score {.inline.} = Score(-20_000)
|
ISOLATED_PAWN_MALUS: array[9, Score] = [0, -10, -25, -50, -75, -75, -75, -75, -75]
|
||||||
func highestEval*: Score {.inline.} = Score(20_000)
|
|
||||||
func mateScore*: Score {.inline.} = lowestEval()
|
|
||||||
|
|
||||||
|
|
||||||
proc getPieceValue(kind: PieceKind): Score =
|
var
|
||||||
## Returns the absolute value of a piece
|
MIDDLEGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]]
|
||||||
case kind:
|
ENDGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
proc getPieceScore*(board: Chessboard, square: Square): Score =
|
|
||||||
## Returns the value of the piece located at
|
proc initializeTables =
|
||||||
## the given square
|
for kind in [Bishop, King, Knight, Pawn, Queen, Rook]:
|
||||||
return board.getPiece(square).kind.getPieceValue()
|
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 =
|
proc getGamePhase(board: Chessboard): int =
|
||||||
|
@ -219,9 +221,33 @@ proc getGamePhase(board: Chessboard): int =
|
||||||
result = min(24, result)
|
result = min(24, result)
|
||||||
|
|
||||||
|
|
||||||
proc evaluatePiecePositions(board: ChessBoard): Score =
|
proc getPieceScore*(board: Chessboard, square: Square): Score =
|
||||||
## Returns the evaluation of the current
|
## Returns the value of the piece located at
|
||||||
## material's position relative to white
|
## 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
|
let
|
||||||
middleGamePhase = board.getGamePhase()
|
middleGamePhase = board.getGamePhase()
|
||||||
endGamePhase = 24 - middleGamePhase
|
endGamePhase = 24 - middleGamePhase
|
||||||
|
@ -232,8 +258,8 @@ proc evaluatePiecePositions(board: ChessBoard): Score =
|
||||||
|
|
||||||
for sq in board.getOccupancy():
|
for sq in board.getOccupancy():
|
||||||
let piece = board.getPiece(sq)
|
let piece = board.getPiece(sq)
|
||||||
middleGameScores[piece.color.int] += MIDDLEGAME_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_TABLES[piece.kind.int][sq.int]
|
endGameScores[piece.color.int] += ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int]
|
||||||
|
|
||||||
let
|
let
|
||||||
sideToMove = board.position.sideToMove
|
sideToMove = board.position.sideToMove
|
||||||
|
@ -244,27 +270,35 @@ proc evaluatePiecePositions(board: ChessBoard): Score =
|
||||||
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
|
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
|
||||||
|
|
||||||
|
|
||||||
proc evaluateMaterial(board: ChessBoard): Score =
|
proc evaluatePawnStructure(board: Chessboard): Score =
|
||||||
## Returns the material evaluation of the
|
## Evaluates the pawn structure of the current
|
||||||
## current position relative to white (positive
|
## position for the side to move
|
||||||
## if in white's favor, negative otherwise)
|
let
|
||||||
var
|
sideToMove = board.position.sideToMove
|
||||||
whiteScore: Score
|
friendlyPawns = board.getOccupancyFor(sideToMove)
|
||||||
blackScore: Score
|
|
||||||
|
|
||||||
for sq in board.getOccupancyFor(White):
|
# Doubled pawns are a bad idea
|
||||||
whiteScore += board.getPieceScore(sq)
|
var doubledPawns = 0
|
||||||
|
for file in 0..7:
|
||||||
for sq in board.getOccupancyFor(Black):
|
if (getFileMask(file) and friendlyPawns).countSquares() > 1:
|
||||||
blackScore += board.getPieceScore(sq)
|
inc(doubledPawns)
|
||||||
|
|
||||||
result = whiteScore - blackScore
|
# Isolated pawns are also a bad idea
|
||||||
if board.position.sideToMove == Black:
|
var isolatedPawns = 0
|
||||||
result *= -1
|
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 =
|
proc evaluate*(board: Chessboard): Score =
|
||||||
## Evaluates the current position
|
## Evaluates the current position
|
||||||
|
|
||||||
result = board.evaluateMaterial()
|
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))
|
moves.add(createMove(square, target, Capture))
|
||||||
|
|
||||||
|
|
||||||
proc generateKingMoves(self: Chessboard, moves: var MoveList) =
|
proc generateKingMoves(self: Chessboard, moves: var MoveList, capturesOnly=false) =
|
||||||
let
|
let
|
||||||
sideToMove = self.position.sideToMove
|
sideToMove = self.position.sideToMove
|
||||||
king = self.getBitboard(King, 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)
|
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:
|
if not capturesOnly:
|
||||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
for square in bitboard and not occupancy:
|
||||||
moves.add(createMove(king, square))
|
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
||||||
|
moves.add(createMove(king, square))
|
||||||
for square in bitboard and enemyPieces:
|
for square in bitboard and enemyPieces:
|
||||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
||||||
moves.add(createMove(king, square, Capture))
|
moves.add(createMove(king, square, Capture))
|
||||||
|
@ -271,12 +272,15 @@ proc generateCastling(self: Chessboard, moves: var MoveList) =
|
||||||
moves.add(createMove(kingSquare, kingPiece.queenSideCastling(), Castle))
|
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
|
## 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
|
let
|
||||||
self.generateKingMoves(moves)
|
sideToMove = self.position.sideToMove
|
||||||
|
nonSideToMove = sideToMove.opposite()
|
||||||
|
self.generateKingMoves(moves, capturesOnly)
|
||||||
if self.position.checkers.countSquares() > 1:
|
if self.position.checkers.countSquares() > 1:
|
||||||
# King is in double check: no need to generate any more
|
# King is in double check: no need to generate any more
|
||||||
# moves
|
# moves
|
||||||
|
@ -304,8 +308,8 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
||||||
let
|
let
|
||||||
checker = self.position.checkers.lowestSquare()
|
checker = self.position.checkers.lowestSquare()
|
||||||
checkerBB = checker.toBitboard()
|
checkerBB = checker.toBitboard()
|
||||||
epTarget = self.position.enPassantSquare
|
# epTarget = self.position.enPassantSquare
|
||||||
checkerPiece = self.getPiece(checker)
|
# checkerPiece = self.getPiece(checker)
|
||||||
destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB
|
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
|
# TODO: This doesn't really work. I've addressed the issue for now, but it's kinda ugly. Find a better
|
||||||
# solution
|
# 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 most pieces, because the move generators won't allow them to move there, but it does matter
|
||||||
# # for pawns
|
# # for pawns
|
||||||
# destinationMask = destinationMask or epTarget.toBitboard()
|
# 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.generatePawnMoves(moves, destinationMask)
|
||||||
self.generateKnightMoves(moves, destinationMask)
|
self.generateKnightMoves(moves, destinationMask)
|
||||||
self.generateRookMoves(moves, destinationMask)
|
self.generateRookMoves(moves, destinationMask)
|
||||||
|
@ -427,7 +435,7 @@ proc doMove*(self: Chessboard, move: Move) =
|
||||||
|
|
||||||
proc isLegal*(self: Chessboard, move: Move): bool {.inline.} =
|
proc isLegal*(self: Chessboard, move: Move): bool {.inline.} =
|
||||||
## Returns whether the given move is legal
|
## Returns whether the given move is legal
|
||||||
var moves = MoveList()
|
var moves = newMoveList()
|
||||||
self.generateMoves(moves)
|
self.generateMoves(moves)
|
||||||
return move in moves
|
return move in moves
|
||||||
|
|
||||||
|
|
|
@ -193,4 +193,10 @@ func toAlgebraic*(self: Move): string =
|
||||||
of PromoteToRook:
|
of PromoteToRook:
|
||||||
result &= "r"
|
result &= "r"
|
||||||
else:
|
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
|
# 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
|
||||||
plyFromRoot*: int8
|
plyFromRoot*: uint8
|
||||||
# Number of half moves since
|
# Number of half moves since
|
||||||
# last piece capture or pawn movement.
|
# last piece capture or pawn movement.
|
||||||
# Used for the 50-move rule
|
# Used for the 50-move rule
|
||||||
|
|
|
@ -33,6 +33,7 @@ type
|
||||||
stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread
|
stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread
|
||||||
board: Chessboard
|
board: Chessboard
|
||||||
bestMoveRoot: Move
|
bestMoveRoot: Move
|
||||||
|
bestRootScore: Score
|
||||||
searchStart: MonoTime
|
searchStart: MonoTime
|
||||||
searchDeadline: MonoTime
|
searchDeadline: MonoTime
|
||||||
nodeCount: uint64
|
nodeCount: uint64
|
||||||
|
@ -40,6 +41,7 @@ type
|
||||||
searchMoves: seq[Move]
|
searchMoves: seq[Move]
|
||||||
previousBestMove: Move
|
previousBestMove: Move
|
||||||
transpositionTable: TTable
|
transpositionTable: TTable
|
||||||
|
currentExtensionCount: uint8
|
||||||
|
|
||||||
|
|
||||||
proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager =
|
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 =
|
proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
|
||||||
## Returns an estimated static score for the move
|
## Returns an estimated static score for the move
|
||||||
result = Score(0)
|
result = Score(0)
|
||||||
|
let
|
||||||
|
sideToMove = self.board.position.sideToMove
|
||||||
|
nonSideToMove = sideToMove.opposite()
|
||||||
if self.previousBestMove != nullMove() and move == self.previousBestMove:
|
if self.previousBestMove != nullMove() and move == self.previousBestMove:
|
||||||
result = highestEval() + 1
|
return highestEval() + 1
|
||||||
elif move.isCapture():
|
if move.isCapture():
|
||||||
# Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker
|
# Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker
|
||||||
# We prioritize moves that capture the most valuable pieces, and as a
|
# 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
|
# 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
|
# is why we multiply the score of the captured piece by 100, to give
|
||||||
# it priority)
|
# it priority)
|
||||||
result = 100 * self.board.getPieceScore(move.targetSquare) -
|
result += 100 * self.board.getPieceScore(move.targetSquare) - self.board.getPieceScore(move.startSquare)
|
||||||
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) =
|
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.} =
|
proc orderer(a, b: Move): int {.closure.} =
|
||||||
return cmp(self.getEstimatedMoveScore(a), self.getEstimatedMoveScore(b))
|
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
|
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))
|
nps = 1000 * (self.nodeCount div max(elapsedMsec, 1))
|
||||||
var logMsg = &"info depth {depth} time {elapsedMsec} nodes {self.nodeCount} nps {nps}"
|
var logMsg = &"info depth {depth} time {elapsedMsec} nodes {self.nodeCount} nps {nps}"
|
||||||
if self.bestMoveRoot != nullMove():
|
if self.bestMoveRoot != nullMove():
|
||||||
logMsg &= &" pv {self.bestMoveRoot.toAlgebraic()}"
|
logMsg &= &" bestmove {self.bestMoveRoot.toAlgebraic()} score {self.bestRootScore}"
|
||||||
echo logMsg
|
echo logMsg
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,7 +127,74 @@ proc shouldStop(self: SearchManager): bool =
|
||||||
return true
|
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
|
## Simple negamax search with alpha-beta pruning
|
||||||
if self.shouldStop():
|
if self.shouldStop():
|
||||||
return
|
return
|
||||||
|
@ -120,8 +210,8 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.
|
||||||
if query.entry.score <= alpha:
|
if query.entry.score <= alpha:
|
||||||
return query.entry.score
|
return query.entry.score
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
return self.board.evaluate()
|
return self.qsearch(0, alpha, beta)
|
||||||
var moves = MoveList()
|
var moves = newMoveList()
|
||||||
var depth = depth
|
var depth = depth
|
||||||
self.board.generateMoves(moves)
|
self.board.generateMoves(moves)
|
||||||
self.reorderMoves(moves)
|
self.reorderMoves(moves)
|
||||||
|
@ -136,46 +226,49 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.
|
||||||
# Stalemate
|
# Stalemate
|
||||||
return Score(0)
|
return Score(0)
|
||||||
var bestScore = lowestEval()
|
var bestScore = lowestEval()
|
||||||
var alpha = alpha
|
var alpha = alpha
|
||||||
for i, move in moves:
|
for i, move in moves:
|
||||||
if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves:
|
if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves:
|
||||||
continue
|
continue
|
||||||
self.board.doMove(move)
|
self.board.doMove(move)
|
||||||
|
var extension = self.getSearchExtension(move)
|
||||||
let zobrist = self.board.position.zobristKey
|
let zobrist = self.board.position.zobristKey
|
||||||
inc(self.nodeCount)
|
inc(self.nodeCount)
|
||||||
# Find the best move for us (worst move
|
# Find the best move for us (worst move
|
||||||
# for our opponent, hence the negative sign)
|
# for our opponent, hence the negative sign)
|
||||||
var score = -self.search(depth - 1, ply + 1, -beta, -alpha)
|
var score: Score
|
||||||
if self.board.position.repetitionDraw:
|
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*
|
# Drawing by repetition is *bad*
|
||||||
score = lowestEval() div 2
|
score = Score(0)
|
||||||
self.board.unmakeMove()
|
self.board.unmakeMove()
|
||||||
# When a search is cancelled or times out, we need
|
# 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
|
# to the root move. This is why the check is duplicated
|
||||||
if self.shouldStop():
|
if self.shouldStop():
|
||||||
return
|
return
|
||||||
bestScore = max(score, bestScore)
|
bestScore = max(score, bestScore)
|
||||||
if score >= beta:
|
let nodeType = if score >= beta: LowerBound elif score <= alpha: UpperBound else: Exact
|
||||||
# If we meet this position again, mark the fact that this score is a
|
self.transpositionTable.store(depth.uint8, score, zobrist, nodeType)
|
||||||
# lower bound for the actual true score of the node (i.e. its score
|
if nodeType == LowerBound:
|
||||||
# will NOT be lower than this)
|
# score >= beta
|
||||||
self.transpositionTable.store(depth.uint8, score, zobrist, LowerBound)
|
|
||||||
# This move was too good for us, opponent will not search it
|
# This move was too good for us, opponent will not search it
|
||||||
break
|
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:
|
if score > alpha:
|
||||||
alpha = score
|
alpha = score
|
||||||
if ply == 0:
|
if ply == 0:
|
||||||
self.bestMoveRoot = move
|
self.bestMoveRoot = move
|
||||||
|
self.bestRootScore = bestScore
|
||||||
return bestScore
|
return bestScore
|
||||||
|
|
||||||
|
|
||||||
|
@ -202,16 +295,16 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes:
|
||||||
# Search the previous best move first
|
# Search the previous best move first
|
||||||
self.previousBestMove = self.bestMoveRoot
|
self.previousBestMove = self.bestMoveRoot
|
||||||
self.search(i, 0, lowestEval(), highestEval())
|
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
|
# Since we always search the best move from the
|
||||||
# previous iteration, we can use partial search
|
# previous iteration, we can use partial search
|
||||||
# results: the engine will either not have changed
|
# results: the engine will either not have changed
|
||||||
# its mind, or it will have found an even better move
|
# its mind, or it will have found an even better move
|
||||||
# in the meantime, which we should obviously use!
|
# in the meantime, which we should obviously use!
|
||||||
result = self.bestMoveRoot
|
result = self.bestMoveRoot
|
||||||
|
let shouldStop = self.shouldStop()
|
||||||
if 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
|
## Initializes a new transposition table of
|
||||||
## size bytes
|
## size bytes
|
||||||
new(result)
|
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 =
|
func getIndex(self: TTable, key: ZobristKey): uint64 =
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import movegen
|
import movegen
|
||||||
|
import eval
|
||||||
import uci
|
import uci
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,12 +29,12 @@ type
|
||||||
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
|
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
|
## Counts (and debugs) the number of legal positions reached after
|
||||||
## the given number of ply
|
## the given number of ply
|
||||||
|
|
||||||
var moves = MoveList()
|
var moves = newMoveList()
|
||||||
board.generateMoves(moves)
|
board.generateMoves(moves, capturesOnly=capturesOnly)
|
||||||
if not bulk:
|
if not bulk:
|
||||||
if len(moves) == 0 and board.inCheck():
|
if len(moves) == 0 and board.inCheck():
|
||||||
result.checkmates = 1
|
result.checkmates = 1
|
||||||
|
@ -146,6 +147,7 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) =
|
||||||
args = command[2].splitWhitespace()
|
args = command[2].splitWhitespace()
|
||||||
bulk = false
|
bulk = false
|
||||||
verbose = false
|
verbose = false
|
||||||
|
captures = false
|
||||||
if args.len() > 1:
|
if args.len() > 1:
|
||||||
var ok = true
|
var ok = true
|
||||||
for arg in args[1..^1]:
|
for arg in args[1..^1]:
|
||||||
|
@ -154,6 +156,8 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) =
|
||||||
bulk = true
|
bulk = true
|
||||||
of "verbose":
|
of "verbose":
|
||||||
verbose = true
|
verbose = true
|
||||||
|
of "captures":
|
||||||
|
captures = true
|
||||||
else:
|
else:
|
||||||
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
||||||
ok = false
|
ok = false
|
||||||
|
@ -164,13 +168,13 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) =
|
||||||
let ply = parseInt(args[0])
|
let ply = parseInt(args[0])
|
||||||
if bulk:
|
if bulk:
|
||||||
let t = cpuTime()
|
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
|
let tot = cpuTime() - t
|
||||||
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
||||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}"
|
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}"
|
||||||
else:
|
else:
|
||||||
let t = cpuTime()
|
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
|
let tot = cpuTime() - t
|
||||||
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
||||||
echo &" - Captures: {data.captures}"
|
echo &" - Captures: {data.captures}"
|
||||||
|
@ -327,6 +331,7 @@ const HELP_TEXT = """Nimfish help menu:
|
||||||
Options:
|
Options:
|
||||||
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
||||||
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
||||||
|
- captures: Only generate capture moves
|
||||||
Example: go perft 5 bulk
|
Example: go perft 5 bulk
|
||||||
- position: Get/set board position
|
- position: Get/set board position
|
||||||
Subcommands:
|
Subcommands:
|
||||||
|
@ -362,6 +367,8 @@ const HELP_TEXT = """Nimfish help menu:
|
||||||
- uci: enter UCI mode
|
- uci: enter UCI mode
|
||||||
- quit: exit
|
- quit: exit
|
||||||
- zobrist: Print the zobrist key for the current position
|
- 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
|
echo board.position.zobristKey.uint64
|
||||||
of "rep":
|
of "rep":
|
||||||
echo board.position.repetitionDraw
|
echo board.position.repetitionDraw
|
||||||
|
of "eval":
|
||||||
|
echo &"Eval: {board.evaluate()}"
|
||||||
else:
|
else:
|
||||||
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
||||||
except IOError:
|
except IOError:
|
||||||
|
|
Loading…
Reference in New Issue