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]
|
positions*: seq[Position]
|
||||||
|
|
||||||
|
|
||||||
proc toFEN*(self: Chessboard): string
|
func toFEN*(self: Chessboard): string
|
||||||
|
|
||||||
|
|
||||||
proc newChessboard*: Chessboard =
|
proc newChessboard*: Chessboard =
|
||||||
|
@ -72,22 +72,22 @@ proc canCastle*(self: Chessboard): tuple[queen, king: bool] {.inline.} =
|
||||||
return self.position.canCastle()
|
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
|
## Returns a colored version of the
|
||||||
## current position for easier visualization
|
## current position for easier visualization
|
||||||
return self.position.pretty()
|
return self.position.pretty()
|
||||||
|
|
||||||
|
|
||||||
proc toFEN*(self: Chessboard): string =
|
func toFEN*(self: Chessboard): string =
|
||||||
## Returns a FEN string of the current
|
## Returns a FEN string of the current
|
||||||
## position in the chessboard
|
## position in the chessboard
|
||||||
return self.position.toFEN()
|
return self.position.toFEN()
|
||||||
|
|
||||||
|
|
||||||
proc drawnByRepetition*(self: Chessboard): bool =
|
func drawnByRepetition*(self: Chessboard): bool =
|
||||||
## Returns whether the current position is a draw
|
## Returns whether the current position is a draw
|
||||||
## by repetition
|
## by repetition
|
||||||
# TODO: Improve this
|
# TODO: Improve this
|
||||||
|
@ -100,3 +100,60 @@ proc drawnByRepetition*(self: Chessboard): bool =
|
||||||
return true
|
return true
|
||||||
dec(i)
|
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]
|
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 =
|
||||||
|
@ -150,21 +149,10 @@ proc shouldStop(self: SearchManager): bool =
|
||||||
proc getSearchExtension(self: SearchManager, move: Move): int {.used.} =
|
proc getSearchExtension(self: SearchManager, move: Move): int {.used.} =
|
||||||
## Returns the number of extensions that should be performed
|
## Returns the number of extensions that should be performed
|
||||||
## when exploring the given move
|
## when exploring the given move
|
||||||
if self.currentExtensionCount == 16:
|
|
||||||
return 0
|
|
||||||
if self.board.inCheck():
|
if self.board.inCheck():
|
||||||
# Opponent is in check: extend the search to see
|
# Opponent is in check: extend the search to see
|
||||||
# if we can do other interesting things!
|
# if we can do other interesting things!
|
||||||
inc(self.currentExtensionCount)
|
|
||||||
return 1
|
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 =
|
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:
|
if score >= beta:
|
||||||
# Same as with the regular alpha-beta search
|
# Same as with the regular alpha-beta search
|
||||||
return score
|
return score
|
||||||
if self.board.position.halfMoveClock >= 100 or self.board.drawnByRepetition():
|
if self.board.isDrawn():
|
||||||
return Score(0)
|
return Score(0)
|
||||||
var moves = newMoveList()
|
var moves = newMoveList()
|
||||||
self.board.generateMoves(moves, capturesOnly=true)
|
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.} =
|
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():
|
if depth > 1 and self.shouldStop():
|
||||||
# We do not let ourselves get cancelled at depth
|
# We do not let ourselves get cancelled at depth
|
||||||
# one because then we wouldn't have a move to return.
|
# 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:
|
of UpperBound:
|
||||||
if query.entry.score <= alpha:
|
if query.entry.score <= alpha:
|
||||||
return query.entry.score
|
return query.entry.score
|
||||||
if self.board.drawnByRepetition():
|
if self.board.isDrawn():
|
||||||
return Score(0)
|
return Score(0)
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
# Quiescent search gain: 264.8 +/- 71.6
|
# 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:
|
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 = 0
|
let extension = self.getSearchExtension(move)
|
||||||
when defined(searchExtensions):
|
|
||||||
extension = self.getSearchExtension(move)
|
|
||||||
inc(self.nodeCount)
|
inc(self.nodeCount)
|
||||||
var score: Score
|
# Find the best move for us (worst move
|
||||||
var fullDepth = true
|
# for our opponent, hence the negative sign)
|
||||||
when defined(searchLMR):
|
let score = -self.search(depth - 1 + extension, ply + 1, -beta, -alpha)
|
||||||
if extension == 0 and i >= 5 and not move.isCapture():
|
# TODO
|
||||||
# Late Move Reductions: assume our move orderer did a good job,
|
# Implementation of Principal Variation Search (PVS)
|
||||||
# so it is not worth it to look at all moves at the same depth equally.
|
# var score: Score
|
||||||
# If this move turns out to be better than we expected, we'll re-search
|
#[if i == 0:
|
||||||
# it at full depth
|
# Due to our move ordering scheme, the first move is always the "best", so
|
||||||
const reduction = 1
|
# search it always at full depth with the full search window
|
||||||
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)
|
|
||||||
score = -self.search(depth - 1 + extension, ply + 1, -beta, -alpha)
|
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()
|
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 unwinds back
|
# to make sure the entire call stack unwinds back
|
||||||
|
|
Loading…
Reference in New Issue