Check extensions (gain 89.5 +/- 34.0) and initial work on draw by insufficient material detection

This commit is contained in:
Mattia Giambirtone 2024-05-03 17:10:18 +02:00
parent 2fabc785aa
commit 79603ff9ec
2 changed files with 95 additions and 37 deletions

View File

@ -39,7 +39,7 @@ type
positions*: seq[Position]
proc toFEN*(self: Chessboard): string
func toFEN*(self: Chessboard): string
proc newChessboard*: Chessboard =
@ -72,22 +72,22 @@ proc canCastle*(self: Chessboard): tuple[queen, king: bool] {.inline.} =
return self.position.canCastle()
proc `$`*(self: Chessboard): string = $self.position
func `$`*(self: Chessboard): string = $self.position
proc pretty*(self: Chessboard): string =
func pretty*(self: Chessboard): string =
## Returns a colored version of the
## current position for easier visualization
return self.position.pretty()
proc toFEN*(self: Chessboard): string =
func toFEN*(self: Chessboard): string =
## Returns a FEN string of the current
## position in the chessboard
return self.position.toFEN()
proc drawnByRepetition*(self: Chessboard): bool =
func drawnByRepetition*(self: Chessboard): bool =
## Returns whether the current position is a draw
## by repetition
# TODO: Improve this
@ -100,3 +100,60 @@ proc drawnByRepetition*(self: Chessboard): bool =
return true
dec(i)
proc isInsufficientMaterial*(self: Chessboard): bool =
## Returns whether the current position is drawn
## due to insufficient mating material. Note that
## this is not a strict implementation of the FIDE
## rule about material draws due to the fact that
## it would be basically impossible to implement those
## efficiently
# Break out early if there's more than 4 pieces on the
# board
let occupancy = self.position.getOccupancy()
if occupancy.countSquares() > 4:
return false
let
sideToMove = self.position.sideToMove
nonSideToMove = sideToMove.opposite()
# Break out early if there's any pawns left on the board
if self.position.getBitboard(Pawn, sideToMove) != 0:
return false
if self.position.getBitboard(Pawn, nonSideToMove) != 0:
return false
# If there's any queens or rooks on the board, break out early too
let
friendlyQueens = self.position.getBitboard(Queen, sideToMove)
enemyQueens = self.position.getBitboard(Queen, nonSideToMove)
friendlyRooks = self.position.getBitboard(Rook, sideToMove)
enemyRooks = self.position.getBitboard(Rook, nonSideToMove)
if (friendlyQueens or enemyQueens or friendlyRooks or enemyRooks).countSquares() != 0:
return false
let
friendlyKing = self.position.getBitboard(King, sideToMove)
enemyKing = self.position.getBitboard(King, nonSideToMove)
if (occupancy and not friendlyKing and not enemyKing).countSquares() == 0:
# Only the two kings are left
return true
# TODO
func isDrawn*(self: Chessboard): bool =
## Returns whether the given position is
## drawn
if self.position.halfMoveClock >= 100:
# Draw by 50 move rule
return true
if self.drawnByRepetition():
return true
if self.isInsufficientMaterial():
return true

View File

@ -42,7 +42,6 @@ type
searchMoves: seq[Move]
previousBestMove: Move
transpositionTable: TTable
currentExtensionCount: uint8
proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager =
@ -150,21 +149,10 @@ proc shouldStop(self: SearchManager): bool =
proc getSearchExtension(self: SearchManager, move: Move): int {.used.} =
## 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.position.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 =
@ -189,7 +177,7 @@ proc qsearch(self: SearchManager, ply: uint8, alpha, beta: Score): Score =
if score >= beta:
# Same as with the regular alpha-beta search
return score
if self.board.position.halfMoveClock >= 100 or self.board.drawnByRepetition():
if self.board.isDrawn():
return Score(0)
var moves = newMoveList()
self.board.generateMoves(moves, capturesOnly=true)
@ -216,7 +204,7 @@ 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
## Negamax search with various optimizations and search features
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.
@ -234,7 +222,7 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
of UpperBound:
if query.entry.score <= alpha:
return query.entry.score
if self.board.drawnByRepetition():
if self.board.isDrawn():
return Score(0)
if depth == 0:
# Quiescent search gain: 264.8 +/- 71.6
@ -261,25 +249,38 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves:
continue
self.board.doMove(move)
var extension = 0
when defined(searchExtensions):
extension = self.getSearchExtension(move)
let extension = self.getSearchExtension(move)
inc(self.nodeCount)
var score: Score
var fullDepth = true
when defined(searchLMR):
if extension == 0 and i >= 5 and not move.isCapture():
# Late Move Reductions: assume our move orderer did a good job,
# so it is not worth it 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:
# Find the best move for us (worst move
# for our opponent, hence the negative sign)
# Find the best move for us (worst move
# for our opponent, hence the negative sign)
let score = -self.search(depth - 1 + extension, ply + 1, -beta, -alpha)
# TODO
# Implementation of Principal Variation Search (PVS)
# var score: Score
#[if i == 0:
# Due to our move ordering scheme, the first move is always the "best", so
# search it always at full depth with the full search window
score = -self.search(depth - 1 + extension, ply + 1, -beta, -alpha)
elif extension == 0 and depth > 3 and i >= 5 and not move.isCapture():
# Late Move Reductions: assume our move orderer did a good job,
# so it is not worth it 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
# We first do a null-window search to see if there's a move that beats alpha
# (we don't care about the actual value, so we search in the range [alpha, alpha + 1]
# to increase the number of cutoffs)
score = -self.search(depth - 1 - reduction, ply + 1, -alpha - 1, -alpha)
if score > alpha:
score = -self.search(depth - 1 + extension, ply + 1, -alpha - 1, -alpha)
else:
# Move wasn't reduced, just do a null window search
score = -self.search(depth - 1 + extension, ply + 1, -alpha - 1, -alpha)
if i > 0 and score > alpha and score < beta:
# The move failed high (and not low, which would mean it was too good for us and
# our opponent wouldn't let us play it) in the null window search, search it
# again with the full depth and full window
score = -self.search(depth - 1 + extension, ply + 1, -beta, -alpha) ]#
self.board.unmakeMove()
# When a search is cancelled or times out, we need
# to make sure the entire call stack unwinds back