I am getting some very strange behaviour with my scoring when it comes to checkmate detection. My perft is working fine so there is nothing wrong with my movegen or make/unmake move functions. The problem is that as soon as it sees a mate it will print the mate score in the output score. For example, in this position with black to move: [d]3r1k1b/5p2/4np2/3R1N2/3P1NPP/2K1PP2/8/8 b - - 0 1
The output from the search function looks like this (minus means that the AI think it is worse):
At depth 3 I assume it sees that playing Nxf4 results in Rxd8# (it needs an extra depth to actually find that black has no moves to play). But shouldn't the score be changed when it finds something better to play for itself? Here are some snippets of my code (just the important parts), I hope you can help me spot where I am thinking wrong.
Iterative deepening (where I save the score from each depth):
Code: Select all
for depth in range(1, max_search_depth + 1):
self.legal_moves = 0
self.ply = 0
self.pv_length = [0]*max_ply
self.pv_table = [[0]*max_ply for _ in range(max_ply)]
# Search the position with negamax to given depth
score = negamax(depth, alpha, beta, False)
# Here I save variables into a dict to be able to print in the GUI
self.print_info[depth-1] = {'depth': str(depth), 'time': f'{self.timer:.2f}', 'nodes': str(self.nodes), 'score': score/100, 'main_line': self.pv_line}
# Break if time has run out, if it has reached at least min given search depth
if (self.timer > s.max_search_time and depth >= self.min_search_depth):
break
Code: Select all
def negamax(depth, alpha, beta):
# Init PV length
self.pv_length[self.ply] = self.ply
# Depth = 0, return value from the quiescence search
if depth == 0:
return self.quiescence(alpha, beta)
# Increment node count
self.nodes += 1
# Is own king in check?
self.is_in_check = self.gamestate.is_square_attacked(self.gamestate.king_pos[self.gamestate.side_to_move], self.gamestate.side_to_move)
# Check extension
if self.is_in_check:
depth += 1
# Get pseudo legal moves
children = mg.gen_moves(self.gamestate)
# Sort the moves
children = self.sort_moves(children)
# If we are following PV line, we want to enable PV-scoring
if self.follow_pv:
self.enable_pv_scoring(children)
# Init legal moves counter and best move so far
self.legal_moves = 0
# Number of moves searched in the moves list
moves_searched = 0
# Negamax recursive loop
for child in children:
# If move is legal, make it. Otherwise move on to the next candidate.
if self.gamestate.make_move(child):
# Increment legal moves and ply
self.legal_moves += 1
self.ply += 1
# Do a normal search
score = -self.negamax(depth - 1, -beta, -alpha)
# Decrement ply and increase number of moves searched
self.ply -= 1
moves_searched += 1
# Take back move
self.gamestate.unmake_move()
# Fail-hard beta cutoff (node fails high)
if score >= beta:
# Store killer moves, only if it is a non-capturing move (used in move sorting)
if mg.extract_capture(child) == 0:
self.killer_moves[1][self.ply] = self.killer_moves[0][self.ply]
self.killer_moves[0][self.ply] = child
return beta
# Found a better move (PV-node)
if score > alpha:
alpha = score
# Store history moves, only if it is a non-capturing move (used in move sorting)
if mg.extract_capture(child) == 0:
self.history_moves[mg.extract_piece_moved(child)][mg.extract_to_square(child)] += depth
# Write PV move to PV table for the given ply
self.pv_table[self.ply][self.ply] = child
# Loop over the next ply
for next_ply in range(self.ply + 1, self.pv_length[self.ply + 1], 1):
# Copy move from deeper ply into current ply's line
self.pv_table[self.ply][next_ply] = self.pv_table[self.ply + 1][next_ply]
# Adjust PV length
self.pv_length[self.ply] = self.pv_length[self.ply + 1]
# If we don't have a legal move to make in the position, check whether it's checkmate or stalemate
if not self.legal_moves:
# Checkmate, return checkmate score
if self.is_in_check:
return -1e9 + self.ply
# Stalemate, return stalemate score
return 0
# Node fails low
return alpha
Code: Select all
if abs(score) >= 1e6:
break
Code: Select all
if self.is_in_check:
return -1e9 + self.ply if self.gamestate.side_to_move == Color.WHITE else 1e9 - self.ply
Very thankful for any input and insights. I am following Maksims tutorial on YouTube, huge thanks to him for helping me with e.g. obtaining PV-line. The checkmate episode is found here