Hide untested options behind when defined(), increase size of some position counters, tweak MVVLVA multiplier and more fixes

This commit is contained in:
Mattia Giambirtone 2024-04-28 16:17:30 +02:00
parent 679965c438
commit d3170ab03a
9 changed files with 51 additions and 43 deletions

3
Chess/.gitignore vendored
View File

@ -9,4 +9,5 @@ nimfish/nimfishpkg/resources/*.pgn
__pycache__ __pycache__
fast-chess fast-chess
log.txt log.txt
config.json config.json
*.log

View File

@ -3,4 +3,3 @@
-d:danger -d:danger
--passL:"-flto" --passL:"-flto"
--passC:"-Ofast -flto -march=native -mtune=native" --passC:"-Ofast -flto -march=native -mtune=native"
--maxLoopIterationsVM:100000000

View File

@ -168,7 +168,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard =
while index <= fen.high(): while index <= fen.high():
s.add(fen[index]) s.add(fen[index])
inc(index) inc(index)
result.position.fullMoveCount = parseInt(s).int8 result.position.fullMoveCount = parseInt(s).uint16
else: else:
raise newException(ValueError, "invalid FEN: too many fields in FEN string") raise newException(ValueError, "invalid FEN: too many fields in FEN string")
inc(index) inc(index)

View File

@ -245,6 +245,7 @@ proc getPieceScore*(board: Chessboard, piece: Piece, square: Square): Score =
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
proc evaluateMaterial(board: ChessBoard): Score = proc evaluateMaterial(board: ChessBoard): Score =
## Returns a material and position evaluation ## Returns a material and position evaluation
## for the current side to move ## for the current side to move
@ -270,7 +271,7 @@ proc evaluateMaterial(board: ChessBoard): Score =
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) 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 ## Evaluates the pawn structure of the current
## position for the side to move ## position for the side to move
let let
@ -294,11 +295,20 @@ proc evaluatePawnStructure(board: Chessboard): Score =
fileMask = fileMask or getFileMask(file + 1) fileMask = fileMask or getFileMask(file + 1)
if (friendlyPawns and fileMask) == 0: if (friendlyPawns and fileMask) == 0:
inc(isolatedPawns) 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 = proc evaluate*(board: Chessboard): Score =
## Evaluates the current position ## Evaluates the current position
result = board.evaluateMaterial() result = board.evaluateMaterial()
result += board.evaluatePawnStructure() when defined(evalPawns):
result += board.evaluatePawnStructure()

View File

@ -480,15 +480,13 @@ proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
const testFens = staticRead("../../tests/all.txt").splitLines() const testFens = staticRead("../../tests/all.txt").splitLines()
const benchFens = staticRead("../../tests/all.txt").splitLines()
proc basicTests* = proc basicTests* =
for fen in testFens: for fen in testFens:
doAssert fen == newChessboardFromFEN(fen).toFEN() doAssert fen == newChessboardFromFEN(fen).toFEN()
for fen in benchFens: for fen in testFens:
var var
board = newChessboardFromFEN(fen) board = newChessboardFromFEN(fen)
hashes = newTable[ZobristKey, Move]() hashes = newTable[ZobristKey, Move]()
@ -497,11 +495,10 @@ proc basicTests* =
for move in moves: for move in moves:
board.makeMove(move) board.makeMove(move)
let let
currentFEN = board.toFEN()
pos = board.position pos = board.position
key = pos.zobristKey key = pos.zobristKey
board.unmakeMove() 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 hashes[key] = move

View File

@ -30,14 +30,14 @@ type
# Number of half-moves that were performed # Number of half-moves that were performed
# to reach this position starting from the # to reach this position starting from the
# root of the tree # root of the tree
plyFromRoot*: uint8 plyFromRoot*: uint16
# Number of half moves since # Number of half moves since
# last piece capture or pawn movement. # last piece capture or pawn movement.
# Used for the 50-move rule # Used for the 50-move rule
halfMoveClock*: int8 halfMoveClock*: int8
# Full move counter. Increments # Full move counter. Increments
# every 2 ply (half-moves) # every 2 ply (half-moves)
fullMoveCount*: int8 fullMoveCount*: uint16
# En passant target square (see https://en.wikipedia.org/wiki/En_passant) # En passant target square (see https://en.wikipedia.org/wiki/En_passant)
enPassantSquare*: Square enPassantSquare*: Square

View File

@ -63,9 +63,9 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
# Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker # Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker
# We prioritize moves that capture the most valuable pieces, and as a # 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 # 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) # 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(): if move.isPromotion():
# Promotions are a good idea to search first # Promotions are a good idea to search first
var piece: Piece var piece: Piece
@ -81,10 +81,11 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
else: else:
discard # Unreachable discard # Unreachable
result += self.board.getPieceScore(piece, move.targetSquare) 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 # Moving on a square attacked by an enemy pawn is _usually_ a very bad
# idea. Assume the piece is lost # idea. Assume the piece is lost and give a malus based on the fact that
result -= self.board.getPieceScore(move.startSquare) # losing a piece this way is a very poor move
result -= self.board.getPieceScore(move.startSquare) * 2
proc reorderMoves(self: SearchManager, moves: var MoveList) = proc reorderMoves(self: SearchManager, moves: var MoveList) =
@ -127,7 +128,7 @@ proc shouldStop(self: SearchManager): bool =
return true 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 ## Returns the number of extensions that should be performed
## when exploring the given move ## when exploring the given move
if self.currentExtensionCount == 16: 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.board.generateMoves(moves, capturesOnly=true)
self.reorderMoves(moves) self.reorderMoves(moves)
var bestScore = score var bestScore = score
var alpha = max(alpha, score) var alpha = max(alpha, score)
for move in moves: for move in moves:
self.board.doMove(move) self.board.doMove(move)
inc(self.nodeCount) 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: 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 = self.getSearchExtension(move) var extension = 0
when defined(searchExtensions):
extension = self.getSearchExtension(move)
inc(self.nodeCount) inc(self.nodeCount)
# Find the best move for us (worst move # Find the best move for us (worst move
# for our opponent, hence the negative sign) # for our opponent, hence the negative sign)
var score: Score var score: Score
#[var fullDepth = true var fullDepth = true
if extension == 0 and i >= 3 and not move.isCapture(): when defined(searchLMR):
# Late Move Reduction: assume our move orderer did a good job, if extension == 0 and i >= 3 and not move.isCapture():
# so it is not worth to look at all moves at the same depth equally. # Late Move Reduction: assume our move orderer did a good job,
# If this move turns out to be better than we expected, we'll re-search # so it is not worth to look at all moves at the same depth equally.
# it at full depth # If this move turns out to be better than we expected, we'll re-search
const reduction = 1 # it at full depth
score = -self.search(depth - 1 - reduction, ply + 1, -beta, -alpha) const reduction = 1
fullDepth = score > alpha]# score = -self.search(depth - 1 - reduction, ply + 1, -beta, -alpha)
fullDepth = score > alpha
if self.board.position.halfMoveClock >= 100 or self.board.position.repetitionDraw: if self.board.position.halfMoveClock >= 100 or self.board.position.repetitionDraw:
# Drawing by repetition is *bad* # Drawing by repetition is *bad*
score = Score(0) score = Score(0)
#if fullDepth: elif fullDepth:
score = -self.search(depth - 1 #[+ extension]#, ply + 1, -beta, -alpha) 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
@ -308,10 +312,8 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes:
# its mind, or it will have found an even better move # its mind, or it will have found an even better move
# in the meantime, which we should obviously use! # in the meantime, which we should obviously use!
result = self.bestMoveRoot result = self.bestMoveRoot
let shouldStop = self.shouldStop() if self.shouldStop():
if shouldStop:
self.log(i - 1) self.log(i - 1)
break
else: else:
self.log(i) self.log(i)
if shouldStop:
break

View File

@ -303,15 +303,14 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.}
var command = args.command var command = args.command
session.searching = true session.searching = true
session.currentSearch = newSearchManager(session.board, session.transpositionTable) session.currentSearch = newSearchManager(session.board, session.transpositionTable)
session.currentSearch.stopFlag.store(false)
var var
timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime) timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime)
increment = (if session.board.position.sideToMove == White: command.winc else: command.binc) increment = (if session.board.position.sideToMove == White: command.winc else: command.binc)
maxTime = (timeRemaining div 20) + (increment div 2) maxTime = (timeRemaining div 20) + (increment div 2)
# Buffer to avoid loosing on time
if maxTime == 0: if maxTime == 0:
maxTime = int32.high() maxTime = int32.high()
else: else:
# Buffer to avoid losing on time
maxTime -= 100 maxTime -= 100
if command.moveTime != -1: if command.moveTime != -1:
maxTime = command.moveTime maxTime = command.moveTime

View File

@ -40,11 +40,11 @@ proc computeZobristKeys: array[781, ZobristKey] =
# to move # to move
result[768] = ZobristKey(prng.next()) result[768] = ZobristKey(prng.next())
# Four numbers to indicate castling rights # Four numbers to indicate castling rights
for i in 769..773: for i in 769..772:
result[i] = ZobristKey(prng.next()) result[i] = ZobristKey(prng.next())
# Eight numbers to indicate the file of a valid # Eight numbers to indicate the file of a valid
# En passant square, if any # En passant square, if any
for i in 774..780: for i in 773..780:
result[i] = ZobristKey(prng.next()) 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 = 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] return ZOBRIST_KEYS[index]