Skip to content

Commit 7bcc2f0

Browse files
luccabbclaude
andcommitted
Improve quiescence search
Enhances quiescence search with better draw detection and check handling: - Add draw detection: fifty-move rule, insufficient material, repetition - Proper check handling: when in check, search all evasions instead of only tactical moves (position is unstable, can't use stand-pat) - Add is_tactical_move() helper for cleaner move filtering - Update organize_moves_quiescence() to use tactical move detection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cb7f635 commit 7bcc2f0

File tree

2 files changed

+75
-26
lines changed

2 files changed

+75
-26
lines changed

moonfish/engines/alpha_beta.py

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,47 +91,77 @@ def quiescence_search(
9191
Returns:
9292
- best_score: returns best move's score.
9393
"""
94-
if board.is_stalemate():
95-
return 0
94+
in_check = board.is_check()
9695

9796
if board.is_checkmate():
9897
return -self.config.checkmate_score
9998

100-
stand_pat = self.eval_board(board)
99+
if board.is_stalemate():
100+
return 0
101101

102-
# recursion base case
103-
if depth == 0:
104-
return stand_pat
102+
# Draw detection: fifty-move rule, insufficient material
103+
# Note: Repetition is checked after making moves, not here
104+
if board.is_fifty_moves() or board.is_insufficient_material():
105+
return 0
105106

106-
# beta-cutoff
107-
if stand_pat >= beta:
108-
return beta
107+
stand_pat = self.eval_board(board)
108+
109+
# When in check, we can't use stand-pat for pruning (position is unstable)
110+
# We must search all evasions. However, still respect depth limit.
111+
if in_check:
112+
# In check: search all evasions, but don't use stand-pat for cutoffs
113+
if depth <= 0:
114+
# At depth limit while in check: return evaluation
115+
# (not ideal but prevents infinite recursion)
116+
return stand_pat
117+
118+
best_score = float("-inf")
119+
moves = list(board.legal_moves) # All evasions
120+
else:
121+
# Not in check: normal quiescence behavior
122+
# recursion base case
123+
if depth <= 0:
124+
return stand_pat
125+
126+
# beta-cutoff: position is already good enough
127+
if stand_pat >= beta:
128+
return beta
109129

110-
# alpha update
111-
alpha = max(alpha, stand_pat)
130+
# Use stand-pat as baseline (we can always choose not to capture)
131+
best_score = stand_pat
132+
alpha = max(alpha, stand_pat)
112133

113-
# get moves for quiescence search
114-
moves = organize_moves_quiescence(board)
134+
# Only tactical moves when not in check
135+
moves = organize_moves_quiescence(board)
115136

116137
for move in moves:
117138
# make move and get score
118139
board.push(move)
119-
score = -self.quiescence_search(
120-
board=board,
121-
depth=depth - 1,
122-
alpha=-beta,
123-
beta=-alpha,
124-
)
140+
141+
# Check if this move leads to a repetition (draw)
142+
if board.is_repetition(2):
143+
score: float = 0 # Draw score
144+
else:
145+
score = -self.quiescence_search(
146+
board=board,
147+
depth=depth - 1,
148+
alpha=-beta,
149+
beta=-alpha,
150+
)
151+
125152
board.pop()
126153

154+
if score > best_score:
155+
best_score = score
156+
127157
# beta-cutoff
128158
if score >= beta:
129159
return beta
130160

131161
# alpha-update
132162
alpha = max(alpha, score)
133163

134-
return alpha
164+
return best_score
135165

136166
def negamax(
137167
self,

moonfish/move_ordering.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,45 @@ def organize_moves(board: Board):
3232
return captures + non_captures
3333

3434

35+
def is_tactical_move(board: Board, move: Move) -> bool:
36+
"""
37+
Check if a move is tactical (should be searched in quiescence).
38+
39+
Tactical moves are:
40+
- Captures (change material)
41+
- Promotions (significant material gain)
42+
- Moves that give check (forcing)
43+
"""
44+
return (
45+
board.is_capture(move) or move.promotion is not None or board.gives_check(move)
46+
)
47+
48+
3549
def organize_moves_quiescence(board: Board):
3650
"""
3751
This function receives a board and it returns a list of all the
3852
possible moves for the current player, sorted by importance.
3953
54+
Only returns tactical moves: captures, promotions, and checks.
55+
4056
Arguments:
4157
- board: chess board state
4258
4359
Returns:
44-
- moves: list of all the possible moves for the current player sorted based on importance.
60+
- moves: list of tactical moves sorted by importance (MVV-LVA).
4561
"""
4662
phase = get_phase(board)
47-
# filter only important moves for quiescence search
48-
captures = filter(
49-
lambda move: board.is_zeroing(move) or board.gives_check(move),
63+
64+
# Filter only tactical moves for quiescence search
65+
# (captures, promotions, checks - NOT quiet pawn pushes)
66+
tactical_moves = filter(
67+
lambda move: is_tactical_move(board, move),
5068
board.legal_moves,
5169
)
52-
# sort moves by importance
70+
71+
# Sort moves by importance using MVV-LVA
5372
moves = sorted(
54-
captures,
73+
tactical_moves,
5574
key=lambda move: mvv_lva(board, move, phase),
5675
reverse=(board.turn == BLACK),
5776
)

0 commit comments

Comments
 (0)