From 9ffa7f6ea6741baef3d460cfebdfc0cbb4ba63b1 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Tue, 7 May 2024 13:40:48 +0200 Subject: [PATCH] Fix searches with time per move, preliminary work on eval improvements, minor changes and fixes --- Chess/nimfish/nimfishpkg/bitboards.nim | 41 +++++++++++++++++++++ Chess/nimfish/nimfishpkg/eval.nim | 50 ++++++++++++++++---------- Chess/nimfish/nimfishpkg/search.nim | 16 +++++---- Chess/nimfish/nimfishpkg/uci.nim | 9 ++--- 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/Chess/nimfish/nimfishpkg/bitboards.nim b/Chess/nimfish/nimfishpkg/bitboards.nim index e2a9800..93c682e 100644 --- a/Chess/nimfish/nimfishpkg/bitboards.nim +++ b/Chess/nimfish/nimfishpkg/bitboards.nim @@ -309,12 +309,53 @@ func computePawnAttacks(color: PieceColor): array[Square(0)..Square(63), Bitboar let pawn = i.toBitboard() result[i] = pawn.backwardLeftRelativeTo(color) or pawn.backwardRightRelativeTo(color) + +func computePassedPawnMasks(color: PieceColor): array[Square(0)..Square(63), Bitboard] = + ## Precomputes all the masks for passed pawns of the + ## given color + for square in Square(0)..Square(63): + let file = fileFromSquare(square) + let rank = rankFromSquare(square) + case color: + of White: + result[square] = (getFileMask(file) shr (8 * (rank))) + if file + 1 in 0..7: + result[square] = result[square] or (getFileMask(file + 1) shr (8 * (rank))) + if file - 1 in 0..7: + result[square] = result[square] or (getFileMask(file - 1) shr (8 * (rank))) + result[square] = result[square] and not getRankMask(0) + of Black: + result[square] = (getFileMask(file) shl (8 * (rank))) + if file + 1 in 0..7: + result[square] = result[square] or (getFileMask(file + 1) shl (8 * (rank))) + if file - 1 in 0..7: + result[square] = result[square] or (getFileMask(file - 1) shl (8 * (rank))) + # Remove last rank + result[square] = result[square] and not getRankMask(7) + else: + discard + + +func computeIsolatedPawnMasks: array[8, Bitboard] {.compileTime.} = + ## Computes all the masks for isolated pawns + for file in 0..7: + if file - 1 in 0..7: + result[file] = result[file] or getFileMask(file - 1) + if file + 1 in 0..7: + result[file] = result[file] or getFileMask(file + 1) + + const KING_BITBOARDS = computeKingBitboards() KNIGHT_BITBOARDS = computeKnightBitboards() PAWN_ATTACKS: array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), Bitboard]] = [computePawnAttacks(White), computePawnAttacks(Black)] + ISOLATED_PAWNS = computeIsolatedPawnMasks() + +let PASSED_PAWNS: array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), Bitboard]] = [computePassedPawnMasks(White), computePassedPawnMasks(Black)] func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square] func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square] func getPawnAttacks*(color: PieceColor, square: Square): Bitboard {.inline.} = PAWN_ATTACKS[color][square] +proc getPassedPawnMask*(color: PieceColor, square: Square): Bitboard {.inline.} = PASSED_PAWNS[color][square] +func getIsolatedPawnMask*(file: int): Bitboard {.inline.} = ISOLATED_PAWNS[file] diff --git a/Chess/nimfish/nimfishpkg/eval.nim b/Chess/nimfish/nimfishpkg/eval.nim index 66cf88c..36f473d 100644 --- a/Chess/nimfish/nimfishpkg/eval.nim +++ b/Chess/nimfish/nimfishpkg/eval.nim @@ -111,7 +111,7 @@ const ] QUEEN_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [ - -28, 0, 29, 12, 59, 44, 43, 45, + -28, 0, 29, 12, 59, 44, 43, 45, -24, -39, -5, 1, -16, 57, 28, 54, -13, -17, 7, 8, 29, 56, 47, 57, -27, -27, -16, -16, -1, 17, -2, 1, @@ -178,7 +178,8 @@ const DOUBLED_PAWNS_MALUS: array[9, Score] = [0, -5, -10, -20, -30, -30, -30, -30, -30] ISOLATED_PAWN_MALUS: array[9, Score] = [0, -10, -25, -50, -75, -75, -75, -75, -75] - + STRONG_PAWNS_BONUS: array[9, Score] = [0, 5, 10, 20, 30, 30, 30, 30, 30] + PASSED_PAWN_BONUS: array[7, Score] = [0, 120, 80, 50, 30, 15, 15] var MIDDLEGAME_VALUE_TABLES: array[PieceColor.White..PieceColor.Black, array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]]] @@ -257,7 +258,7 @@ proc evaluateMaterial(position: Position): Score = # White, Black middleGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0] endGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0] - + for sq in position.getOccupancy(): let piece = position.getPiece(sq) middleGameScores[piece.color] += MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][sq] @@ -275,9 +276,12 @@ proc evaluateMaterial(position: Position): Score = proc evaluatePawnStructure(position: Position): Score {.used.} = ## Evaluates the pawn structure of the current ## position for the side to move + + # TODO: Don't use this. Make it tapered and tune it let sideToMove = position.sideToMove - friendlyPawns = position.getOccupancyFor(sideToMove) + friendlyPawns = position.getBitboard(Pawn, sideToMove) + enemyPawns = position.getBitboard(Pawn, sideToMove.opposite()) # Doubled pawns are a bad idea var doubledPawns = 0 @@ -287,23 +291,29 @@ proc evaluatePawnStructure(position: Position): Score {.used.} = # Isolated pawns are also a bad idea var isolatedPawns = 0 + # Passed pawns are good + var passedPawnBonus = Score(0) 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) - # Pawns that are defended by another pawn are - # stronger - var strongPawnIncrement = Score(0) - for pawn in position.getBitboard(Pawn, White): - if position.getPawnAttacks(pawn, White) != 0: - strongPawnIncrement += position.getPieceScore(pawn) div Score(4) + let rank = rankFromSquare(pawn) - return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns] + strongPawnIncrement + if (friendlyPawns and getIsolatedPawnMask(file)) == 0: + inc(isolatedPawns) + + if (getPassedPawnMask(sideToMove, pawn) and enemyPawns) == 0: + let distanceFromPromotion = if sideToMove == White: rank else: 7 - rank + passedPawnBonus += PASSED_PAWN_BONUS[distanceFromPromotion] + + # Nice (and cheap!) bitboard trick to count how many of our friendly pawns are defended + # by other friendly pawns + let strongPawns = ((friendlyPawns shl 7 or friendlyPawns shl 9) and friendlyPawns).countSquares() + + return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns] + STRONG_PAWNS_BONUS[strongPawns] + passedPawnBonus + + +proc evaluateThreats(position: Position): Score {.used.} = + ## Evaluates threats in the current position + # TODO proc evaluate*(position: Position): Score = @@ -312,5 +322,7 @@ proc evaluate*(position: Position): Score = result = position.evaluateMaterial() when defined(evalPawns): result += position.evaluatePawnStructure() + when defined(evalThreats): + result += position.evaluateThreats() # Tempo bonus: gains 19.5 +/- 13.7 - result += TEMPO_BONUS + result += TEMPO_BONUS \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index 9b1274d..13d0048 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -205,8 +205,7 @@ proc getSearchExtension(self: SearchManager, move: Move): int {.used.} = proc getReduction(self: SearchManager, move: Move, depth, ply, moveNumber: int, isPV: bool): int = - ## Returns the amount a search depth can be reduced to - + ## Returns the amount a search depth can be reduced to # Move reduction gains: 54.1 +/- 25.5 if moveNumber > 2 and depth > 2: result = LMR_TABLE[depth][moveNumber] @@ -427,7 +426,7 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV: return bestScore -proc findBestMove*(self: var SearchManager, timeRemaining, increment: int64, maxDepth: int, maxNodes: uint64, searchMoves: seq[Move]): Move = +proc findBestMove*(self: var SearchManager, timeRemaining, increment: int64, maxDepth: int, maxNodes: uint64, searchMoves: seq[Move], timePerMove=false): Move = ## Finds the best move in the current position ## and returns it, limiting search time according ## to the remaining time and increment values provided @@ -437,13 +436,16 @@ proc findBestMove*(self: var SearchManager, timeRemaining, increment: int64, max ## nodes. If searchMoves is provided and is not empty, search will ## be restricted to the moves in the list. Note that regardless of ## any time limitations or explicit cancellations, the search will - ## not stop until it has at least cleared depth one. Search depth - ## is always constrained to at most MAX_DEPTH ply from the root + ## not stop until it has at least cleared depth one. Search depth + ## is always constrained to at most MAX_DEPTH ply from the root. If + ## timePerMove is true, the increment is assumed to be zero and the + ## remaining time is considered the time limit for the entire search + ## (note that soft time management is disabled in that case) # Apparently negative remaining time is a thing. Welp let - maxSearchTime = max(1, (timeRemaining div 10) + (increment div 2)) - softLimit = maxSearchTime div 3 + maxSearchTime = if not timePerMove: max(1, (timeRemaining div 10) + (increment div 2)) else: timeRemaining + softLimit = if not timePerMove: maxSearchTime div 3 else: maxSearchTime result = nullMove() self.maxNodes = maxNodes self.searchMoves = searchMoves diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index 7a6736c..d51cb29 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -324,12 +324,13 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} var timeRemaining = (if session.position.sideToMove == White: command.wtime else: command.btime) increment = (if session.position.sideToMove == White: command.winc else: command.binc) - if command.moveTime != -1: - timeRemaining = 0 - increment = command.moveTime + timePerMove = command.moveTime != -1 + if timePerMove: + timeRemaining = command.moveTime + increment = 0 elif timeRemaining == 0: timeRemaining = int32.high() - var move = searcher.findBestMove(timeRemaining, increment, command.depth, command.nodes, command.searchmoves) + var move = searcher.findBestMove(timeRemaining, increment, command.depth, command.nodes, command.searchmoves, timePerMove) echo &"bestmove {move.toAlgebraic()}"