From c7ad3aa1b549061a2b2b79bc753e43657fd2a7d3 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Sun, 28 Apr 2024 16:17:30 +0200 Subject: [PATCH] Hide untested options behind when defined(), increase size of some position counters, tweak MVVLVA multiplier and more fixes --- Chess/.gitignore | 3 +- Chess/nim.cfg | 1 - Chess/nimfish/nimfishpkg/board.nim | 2 +- Chess/nimfish/nimfishpkg/eval.nim | 18 +++++++--- Chess/nimfish/nimfishpkg/movegen.nim | 7 ++-- Chess/nimfish/nimfishpkg/position.nim | 4 +-- Chess/nimfish/nimfishpkg/search.nim | 50 ++++++++++++++------------- Chess/nimfish/nimfishpkg/uci.nim | 3 +- Chess/nimfish/nimfishpkg/zobrist.nim | 6 ++-- 9 files changed, 51 insertions(+), 43 deletions(-) diff --git a/Chess/.gitignore b/Chess/.gitignore index e08faa4..784eca8 100644 --- a/Chess/.gitignore +++ b/Chess/.gitignore @@ -9,4 +9,5 @@ nimfish/nimfishpkg/resources/*.pgn __pycache__ fast-chess log.txt -config.json \ No newline at end of file +config.json +*.log diff --git a/Chess/nim.cfg b/Chess/nim.cfg index 2ce2caa..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" ---maxLoopIterationsVM:100000000 \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim index fbdc16e..4035ba3 100644 --- a/Chess/nimfish/nimfishpkg/board.nim +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -168,7 +168,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard = while index <= fen.high(): s.add(fen[index]) inc(index) - result.position.fullMoveCount = parseInt(s).int8 + result.position.fullMoveCount = parseInt(s).uint16 else: raise newException(ValueError, "invalid FEN: too many fields in FEN string") inc(index) diff --git a/Chess/nimfish/nimfishpkg/eval.nim b/Chess/nimfish/nimfishpkg/eval.nim index 5de265f..54e066c 100644 --- a/Chess/nimfish/nimfishpkg/eval.nim +++ b/Chess/nimfish/nimfishpkg/eval.nim @@ -245,6 +245,7 @@ proc getPieceScore*(board: Chessboard, piece: Piece, square: Square): Score = result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) + proc evaluateMaterial(board: ChessBoard): Score = ## Returns a material and position evaluation ## for the current side to move @@ -270,7 +271,7 @@ proc evaluateMaterial(board: ChessBoard): Score = result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) -proc evaluatePawnStructure(board: Chessboard): Score = +proc evaluatePawnStructure(board: Chessboard): Score {.used.} = ## Evaluates the pawn structure of the current ## position for the side to move let @@ -294,11 +295,20 @@ proc evaluatePawnStructure(board: Chessboard): Score = fileMask = fileMask or getFileMask(file + 1) if (friendlyPawns and fileMask) == 0: inc(isolatedPawns) - return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns] - + # Pawns that are defended by another pawn are + # stronger + var + strongPawnIncrement = Score(0) + for pawn in board.getBitboard(Pawn, White): + if board.getPawnAttacks(pawn, White) != 0: + strongPawnIncrement += board.getPieceScore(pawn) div Score(4) + + return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns] + strongPawnIncrement + proc evaluate*(board: Chessboard): Score = ## Evaluates the current position result = board.evaluateMaterial() - result += board.evaluatePawnStructure() \ No newline at end of file + when defined(evalPawns): + result += board.evaluatePawnStructure() diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 80821c2..3484f35 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -480,15 +480,13 @@ proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) = const testFens = staticRead("../../tests/all.txt").splitLines() -const benchFens = staticRead("../../tests/all.txt").splitLines() - proc basicTests* = for fen in testFens: doAssert fen == newChessboardFromFEN(fen).toFEN() - for fen in benchFens: + for fen in testFens: var board = newChessboardFromFEN(fen) hashes = newTable[ZobristKey, Move]() @@ -497,11 +495,10 @@ proc basicTests* = for move in moves: board.makeMove(move) let - currentFEN = board.toFEN() pos = board.position key = pos.zobristKey board.unmakeMove() - doAssert not hashes.contains(key), &"{fen} has zobrist collisions {move} -> {hashes[key]}" + doAssert not hashes.contains(key), &"{fen} has zobrist collisions {move} -> {hashes[key]} (key is {key.uint64})" hashes[key] = move diff --git a/Chess/nimfish/nimfishpkg/position.nim b/Chess/nimfish/nimfishpkg/position.nim index 30c18cf..2fbf9da 100644 --- a/Chess/nimfish/nimfishpkg/position.nim +++ b/Chess/nimfish/nimfishpkg/position.nim @@ -30,14 +30,14 @@ type # Number of half-moves that were performed # to reach this position starting from the # root of the tree - plyFromRoot*: uint8 + plyFromRoot*: uint16 # Number of half moves since # last piece capture or pawn movement. # Used for the 50-move rule halfMoveClock*: int8 # Full move counter. Increments # every 2 ply (half-moves) - fullMoveCount*: int8 + fullMoveCount*: uint16 # En passant target square (see https://en.wikipedia.org/wiki/En_passant) enPassantSquare*: Square diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index 323d0bd..8fbd723 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -63,9 +63,9 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = # Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker # We prioritize moves that capture the most valuable pieces, and as a # second goal we want to use our least valuable pieces to do so (this - # is why we multiply the score of the captured piece by 100, to give + # is why we multiply the score of the captured piece by 10, to give # it priority) - result += 100 * self.board.getPieceScore(move.targetSquare) - self.board.getPieceScore(move.startSquare) + result += 10 * self.board.getPieceScore(move.targetSquare) - self.board.getPieceScore(move.startSquare) if move.isPromotion(): # Promotions are a good idea to search first var piece: Piece @@ -81,10 +81,11 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = else: discard # Unreachable result += self.board.getPieceScore(piece, move.targetSquare) - if (self.board.getPawnAttacks(move.targetSquare, nonSideToMove) and self.board.getBitboard(Pawn, nonSideToMove)) != 0: + if self.board.getPawnAttacks(move.targetSquare, nonSideToMove) != 0: # Moving on a square attacked by an enemy pawn is _usually_ a very bad - # idea. Assume the piece is lost - result -= self.board.getPieceScore(move.startSquare) + # idea. Assume the piece is lost and give a malus based on the fact that + # losing a piece this way is a very poor move + result -= self.board.getPieceScore(move.startSquare) * 2 proc reorderMoves(self: SearchManager, moves: var MoveList) = @@ -127,7 +128,7 @@ proc shouldStop(self: SearchManager): bool = return true -proc getSearchExtension(self: SearchManager, move: Move): int = +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: @@ -172,7 +173,7 @@ proc qsearch(self: SearchManager, ply: uint8, alpha, beta: Score): Score = self.board.generateMoves(moves, capturesOnly=true) self.reorderMoves(moves) var bestScore = score - var alpha = max(alpha, score) + var alpha = max(alpha, score) for move in moves: self.board.doMove(move) inc(self.nodeCount) @@ -238,25 +239,28 @@ 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 = self.getSearchExtension(move) + var extension = 0 + when defined(searchExtensions): + extension = self.getSearchExtension(move) inc(self.nodeCount) # Find the best move for us (worst move # for our opponent, hence the negative sign) var score: Score - #[var fullDepth = true - if extension == 0 and i >= 3 and not move.isCapture(): - # Late Move Reduction: assume our move orderer did a good job, - # so it is not worth 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]# + var fullDepth = true + when defined(searchLMR): + if extension == 0 and i >= 3 and not move.isCapture(): + # Late Move Reduction: assume our move orderer did a good job, + # so it is not worth 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 self.board.position.halfMoveClock >= 100 or self.board.position.repetitionDraw: # Drawing by repetition is *bad* score = Score(0) - #if fullDepth: - score = -self.search(depth - 1 #[+ extension]#, ply + 1, -beta, -alpha) + elif fullDepth: + 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 @@ -308,10 +312,8 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes: # its mind, or it will have found an even better move # in the meantime, which we should obviously use! result = self.bestMoveRoot - let shouldStop = self.shouldStop() - if shouldStop: + if self.shouldStop(): self.log(i - 1) + break else: - self.log(i) - if shouldStop: - break \ No newline at end of file + self.log(i) \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index f678ede..87f3d8d 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -303,15 +303,14 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} var command = args.command session.searching = true 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) increment = (if session.board.position.sideToMove == White: command.winc else: command.binc) maxTime = (timeRemaining div 20) + (increment div 2) - # Buffer to avoid loosing on time if maxTime == 0: maxTime = int32.high() else: + # Buffer to avoid losing on time maxTime -= 100 if command.moveTime != -1: maxTime = command.moveTime diff --git a/Chess/nimfish/nimfishpkg/zobrist.nim b/Chess/nimfish/nimfishpkg/zobrist.nim index 19e08af..9203906 100644 --- a/Chess/nimfish/nimfishpkg/zobrist.nim +++ b/Chess/nimfish/nimfishpkg/zobrist.nim @@ -40,11 +40,11 @@ proc computeZobristKeys: array[781, ZobristKey] = # to move result[768] = ZobristKey(prng.next()) # Four numbers to indicate castling rights - for i in 769..773: + for i in 769..772: result[i] = ZobristKey(prng.next()) # Eight numbers to indicate the file of a valid # En passant square, if any - for i in 774..780: + for i in 773..780: result[i] = ZobristKey(prng.next()) @@ -54,7 +54,7 @@ const PIECE_TO_INDEX = [[0, 1, 2, 3, 4, 5], [6, 7, 8, 9, 10, 11]] proc getKey*(piece: Piece, square: Square): ZobristKey = - let index = PIECE_TO_INDEX[piece.color.int][piece.kind.int] + square.int + let index = PIECE_TO_INDEX[piece.color.int][piece.kind.int] * 64 + square.int return ZOBRIST_KEYS[index]