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 4efde83934
commit c7ad3aa1b5
9 changed files with 51 additions and 43 deletions

3
Chess/.gitignore vendored
View File

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

View File

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

View File

@ -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)

View File

@ -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()
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 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

View File

@ -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

View File

@ -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
self.log(i)

View File

@ -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

View File

@ -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]