Skip to content

Commit df8a32b

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 5f456a9 commit df8a32b

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
@@ -104,47 +104,77 @@ def quiescence_search(
104104
Returns:
105105
- best_score: returns best move's score.
106106
"""
107-
if board.is_stalemate():
108-
return 0
107+
in_check = board.is_check()
109108

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

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

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

119-
# beta-cutoff
120-
if stand_pat >= beta:
121-
return beta
139+
# beta-cutoff: position is already good enough
140+
if stand_pat >= beta:
141+
return beta
122142

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

126-
# get moves for quiescence search
127-
moves = organize_moves_quiescence(board)
147+
# Only tactical moves when not in check
148+
moves = organize_moves_quiescence(board)
128149

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

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

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

147-
return alpha
177+
return best_score
148178

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