Skip to content

Commit e8b9ff6

Browse files
luccabbclaude
andcommitted
[4/9] Improve quiescence search
Enhances quiescence search with better handling of special positions: **Draw Detection:** - Check fifty-move rule and insufficient material - Detect repetitions when making moves (return 0 for draws) **Check Handling:** - When in check, search ALL legal moves (evasions), not just captures - Can't use stand-pat for pruning when in check (position is unstable) - Separate best_score tracking for in-check vs normal positions **Tactical Move Detection:** - Add `is_tactical_move()` helper function - Tactical moves: captures, promotions, and checks - Previously included quiet pawn pushes (is_zeroing), now more precise **Move Ordering:** - Update `organize_moves_quiescence()` to use is_tactical_move - Sort tactical moves by MVV-LVA Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 82526b0 commit e8b9ff6

2 files changed

Lines changed: 74 additions & 25 deletions

File tree

moonfish/engines/alpha_beta.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,47 +105,77 @@ def quiescence_search(
105105
Returns:
106106
- best_score: returns best move's score.
107107
"""
108-
if board.is_stalemate():
109-
return 0
108+
in_check = board.is_check()
110109

111110
if board.is_checkmate():
112111
return -self.config.checkmate_score
113112

113+
if board.is_stalemate():
114+
return 0
115+
116+
# Draw detection: fifty-move rule, insufficient material
117+
# Note: Repetition is checked after making moves, not here
118+
if board.is_fifty_moves() or board.is_insufficient_material():
119+
return 0
120+
114121
stand_pat = self.eval_board(board)
115122

116-
# recursion base case
117-
if depth == 0:
118-
return stand_pat
123+
# When in check, we can't use stand-pat for pruning (position is unstable)
124+
# We must search all evasions. However, still respect depth limit.
125+
if in_check:
126+
# In check: search all evasions, but don't use stand-pat for cutoffs
127+
if depth <= 0:
128+
# At depth limit while in check: return evaluation
129+
# (not ideal but prevents infinite recursion)
130+
return stand_pat
131+
132+
best_score = float("-inf")
133+
moves = list(board.legal_moves) # All evasions
134+
else:
135+
# Not in check: normal quiescence behavior
136+
# recursion base case
137+
if depth <= 0:
138+
return stand_pat
119139

120-
# beta-cutoff
121-
if stand_pat >= beta:
122-
return beta
140+
# beta-cutoff: position is already good enough
141+
if stand_pat >= beta:
142+
return beta
123143

124-
# alpha update
125-
alpha = max(alpha, stand_pat)
144+
# Use stand-pat as baseline (we can always choose not to capture)
145+
best_score = stand_pat
146+
alpha = max(alpha, stand_pat)
126147

127-
# get moves for quiescence search
128-
moves = organize_moves_quiescence(board)
148+
# Only tactical moves when not in check
149+
moves = organize_moves_quiescence(board)
129150

130151
for move in moves:
131152
# make move and get score
132153
board.push(move)
133-
score = -self.quiescence_search(
134-
board=board,
135-
depth=depth - 1,
136-
alpha=-beta,
137-
beta=-alpha,
138-
)
154+
155+
# Check if this move leads to a repetition (draw)
156+
if board.is_repetition(2):
157+
score: float = 0 # Draw score
158+
else:
159+
score = -self.quiescence_search(
160+
board=board,
161+
depth=depth - 1,
162+
alpha=-beta,
163+
beta=-alpha,
164+
)
165+
139166
board.pop()
140167

168+
if score > best_score:
169+
best_score = score
170+
141171
# beta-cutoff
142172
if score >= beta:
143173
return beta
144174

145175
# alpha-update
146176
alpha = max(alpha, score)
147177

148-
return alpha
178+
return best_score
149179

150180
def negamax(
151181
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)