From 3f283932d850fdeb255ddd6fe0234f767b854a93 Mon Sep 17 00:00:00 2001 From: Mattia Giambirtone Date: Fri, 26 Apr 2024 11:41:59 +0200 Subject: [PATCH] Fix some bugs in movegen. Untested TT Signed-off-by: Mattia Giambirtone --- Chess/nim.cfg | 1 - Chess/nimfish.nimble | 1 + Chess/nimfish/nimfishpkg/magics.nim | 1 - Chess/nimfish/nimfishpkg/movegen.nim | 8 +---- Chess/nimfish/nimfishpkg/search.nim | 31 +++++++++++++++++- Chess/nimfish/nimfishpkg/transpositions.nim | 35 ++++++++++----------- Chess/nimfish/nimfishpkg/uci.nim | 12 +++++-- Chess/nimfish/nimfishpkg/zobrist.nim | 4 +-- 8 files changed, 59 insertions(+), 34 deletions(-) diff --git a/Chess/nim.cfg b/Chess/nim.cfg index 4c13138..b95609e 100644 --- a/Chess/nim.cfg +++ b/Chess/nim.cfg @@ -3,4 +3,3 @@ -d:danger --passL:"-flto" --passC:"-Ofast -flto -march=native -mtune=native" ---threads:on \ No newline at end of file diff --git a/Chess/nimfish.nimble b/Chess/nimfish.nimble index c0711dd..c38d045 100644 --- a/Chess/nimfish.nimble +++ b/Chess/nimfish.nimble @@ -14,6 +14,7 @@ bin = @["nimfish"] requires "nim >= 2.0.4" requires "jsony >= 1.1.5" +requires "nint128 >= 0.3.3" task test, "Runs the test suite": diff --git a/Chess/nimfish/nimfishpkg/magics.nim b/Chess/nimfish/nimfishpkg/magics.nim index 0e0d829..33a967e 100644 --- a/Chess/nimfish/nimfishpkg/magics.nim +++ b/Chess/nimfish/nimfishpkg/magics.nim @@ -369,7 +369,6 @@ else: result = joinPath(result.parentDir(), "resources") const path = buildPath() - echo "Loading magic bitboards" const magicFile = staticRead(joinPath(path, "magics.json")) movesFile = staticRead(joinPath(path, "movesets.json")) diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 96707b4..1857a6e 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -274,12 +274,7 @@ proc generateCastling(self: Chessboard, moves: var MoveList) = proc generateMoves*(self: Chessboard, moves: var MoveList) = ## Generates the list of all possible legal moves ## in the current position - if self.position.halfMoveClock >= 100: - # Draw by 50-move rule - return - # TODO: Check for draw by insufficient material - if self.position.repetitionDraw: - return + let sideToMove = self.position.sideToMove self.generateKingMoves(moves) if self.position.checkers.countSquares() > 1: @@ -440,7 +435,6 @@ proc isLegal*(self: Chessboard, move: Move): bool {.inline.} = proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move - echo move # Updates checks and pins for the side to move if not self.isLegal(move): return nullMove() diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index ee4c47d..f8ae60c 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -16,6 +16,7 @@ import board import movegen import eval +import transpositions import std/times @@ -38,12 +39,14 @@ type maxNodes: uint64 searchMoves: seq[Move] previousBestMove: Move + transpositionTable: TTable -proc newSearchManager*(board: Chessboard): SearchManager = +proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager = new(result) result.board = board result.bestMoveRoot = nullMove() + result.transpositionTable = transpositions proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = @@ -105,9 +108,21 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {. ## Simple negamax search with alpha-beta pruning if self.shouldStop(): return + let query = self.transpositionTable.get(self.board.position.zobristKey, depth.uint8) + if query.success: + case query.entry.flag: + of Exact: + return query.entry.score + of LowerBound: + if query.entry.score >= beta: + return query.entry.score + of UpperBound: + if query.entry.score <= alpha: + return query.entry.score if depth == 0: return self.board.evaluate() var moves = MoveList() + var depth = depth self.board.generateMoves(moves) self.reorderMoves(moves) if moves.len() == 0: @@ -126,6 +141,7 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {. if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves: continue self.board.doMove(move) + let zobrist = self.board.position.zobristKey inc(self.nodeCount) # Find the best move for us (worst move # for our opponent, hence the negative sign) @@ -141,8 +157,21 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {. return bestScore = max(score, bestScore) if score >= beta: + # If we meet this position again, mark the fact that this score is a + # lower bound for the actual true score of the node (i.e. its score + # will NOT be lower than this) + self.transpositionTable.store(depth.uint8, score, zobrist, LowerBound) # This move was too good for us, opponent will not search it break + if score <= alpha: + # If we meet this position again, mark the fact that this score is an + # upper bound for the actual true score of the node (i.e. its score + # will NOT be higher than this) + self.transpositionTable.store(depth.uint8, score, zobrist, UpperBound) + else: + # The position didn't cause any cutoffs, so the score stored here is + # the actual true score of the position + self.transpositionTable.store(depth.uint8, score, zobrist, Exact) if score > alpha: alpha = score if ply == 0: diff --git a/Chess/nimfish/nimfishpkg/transpositions.nim b/Chess/nimfish/nimfishpkg/transpositions.nim index e44ac0c..d3d124b 100644 --- a/Chess/nimfish/nimfishpkg/transpositions.nim +++ b/Chess/nimfish/nimfishpkg/transpositions.nim @@ -15,7 +15,6 @@ ## Implementation of a transposition table import zobrist -import moves import eval @@ -23,7 +22,7 @@ import nint128 type - TTentryFlag = enum + TTentryFlag* = enum ## A flag for an entry in the ## transposition table Exact = 0'i8 @@ -38,18 +37,18 @@ type # fit into an int16 score*: int16 hash*: ZobristKey - bestMove*: Move + depth: uint8 - TTable = ref object - data: ptr UncheckedArray[TTEntry] + TTable* = ref object + data: seq[TTEntry] size: uint64 -proc newTranspositionTable(size: uint64): TTable = +proc newTranspositionTable*(size: uint64): TTable = ## Initializes a new transposition table of ## size bytes - let size = size div sizeof(TTEntry).uint64 - result.data = cast[ptr UncheckedArray[TTEntry]](alloc(size)) + new(result) + result.data = newSeq[TTEntry](size) func getIndex(self: TTable, key: ZobristKey): uint64 = @@ -65,20 +64,18 @@ func getIndex(self: TTable, key: ZobristKey): uint64 = result = (u128(key.uint64) * u128(self.size)).hi -func store(self: TTable, score: Score, hash: ZobristKey, bestMove: Move, flag: TTentryFlag) = +func store*(self: TTable, depth: uint8, score: Score, hash: ZobristKey, flag: TTentryFlag) = ## Stores an entry in the transposition table - self.data[self.getIndex(hash)] = TTEntry(flag: flag, score: int16(score), hash: hash, bestMove: bestMove) + self.data[self.getIndex(hash)] = TTEntry(flag: flag, score: int16(score), hash: hash, depth: depth) -func get(self: TTable, hash: ZobristKey): tuple[success: bool, entry: TTEntry] = +proc get*(self: TTable, hash: ZobristKey, depth: uint8): tuple[success: bool, entry: TTEntry] = ## Attempts to get the entry with the given - ## zobrist key in the transposition table. - ## The success parameter is set to false upon - ## detection of a hash collision and the result - ## should be considered invalid unless it's true + ## zobrist key at the given depth in the table. + ## The success parameter is set to false upon detection + ## of a hash collision or if the provided depth is greater + ## than the one stored in the table: the result should be + ## considered invalid unless it's true result.entry = self.data[self.getIndex(hash)] - result.success = result.entry.hash == hash + result.success = result.entry.hash == hash and result.entry.depth >= depth - -proc `destroy=`(self: TTable) = - dealloc(self.data) diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index 0c6b4bb..52f0451 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -21,6 +21,7 @@ import std/atomics import board import movegen import search +import transpositions type @@ -30,6 +31,7 @@ type searching: bool currentSearch: SearchManager hashTableSize: uint64 + transpositionTable: TTable UCICommandType = enum Unknown, @@ -293,9 +295,13 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} setControlCHook(proc () {.noconv.} = quit(0)) # Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread var session = args.session - var command = args.command + if session.transpositionTable.isNil(): + if session.debug: + echo &"info string created {session.hashTableSize} MiB TT" + session.transpositionTable = newTranspositionTable(session.hashTableSize * 1024 * 1024) + var command = args.command session.searching = true - session.currentSearch = newSearchManager(session.board) + session.currentSearch = newSearchManager(session.board, session.transpositionTable) session.currentSearch.stopFlag.store(false) var timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime) @@ -327,7 +333,7 @@ proc startUCISession* = var cmd: UCICommand cmdStr: string - session = UCISession() + session = UCISession(hashTableSize: 64) while true: try: cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '}) diff --git a/Chess/nimfish/nimfishpkg/zobrist.nim b/Chess/nimfish/nimfishpkg/zobrist.nim index 6209ff1..19e08af 100644 --- a/Chess/nimfish/nimfishpkg/zobrist.nim +++ b/Chess/nimfish/nimfishpkg/zobrist.nim @@ -44,7 +44,7 @@ proc computeZobristKeys: array[781, ZobristKey] = result[i] = ZobristKey(prng.next()) # Eight numbers to indicate the file of a valid # En passant square, if any - for i in 774..781: + for i in 774..780: result[i] = ZobristKey(prng.next()) @@ -81,4 +81,4 @@ proc getKingSideCastlingKey*(color: PieceColor): ZobristKey = discard -proc getEnPassantKey*(file: SomeInteger): ZobristKey = ZOBRIST_KEYS[774 + file] \ No newline at end of file +proc getEnPassantKey*(file: SomeInteger): ZobristKey = ZOBRIST_KEYS[773 + file] \ No newline at end of file