Check extensions (gain 89.5 +/- 34.0) and initial work on draw by insufficient material detection
This commit is contained in:
parent
2fabc785aa
commit
79603ff9ec
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue