From 98668ae66e79c1a2717735122c247341733ee2da Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Fri, 3 May 2024 17:10:18 +0200 Subject: [PATCH] Check extensions (gain 89.5 +/- 34.0) and initial work on draw by insufficient material detection --- Chess/nimfish/nimfishpkg/board.nim | 67 ++++++++++++++++++++++++++--- Chess/nimfish/nimfishpkg/search.nim | 65 ++++++++++++++-------------- 2 files changed, 95 insertions(+), 37 deletions(-) diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim index 21ca21d..f4e7348 100644 --- a/Chess/nimfish/nimfishpkg/board.nim +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -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 diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index 0e497e7..2754cb7 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -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