Skip to content

Commit 845a25a

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 <[email protected]>
1 parent 0291600 commit 845a25a

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
@@ -77,47 +77,77 @@ def quiescence_search(
7777
Returns:
7878
- best_score: returns best move's score.
7979
"""
80-
if board.is_stalemate():
81-
return 0
80+
in_check = board.is_check()
8281

8382
if board.is_checkmate():
8483
return -self.config.checkmate_score
8584

86-
stand_pat = self.eval_board(board)
85+
if board.is_stalemate():
86+
return 0
8787

88-
# recursion base case
89-
if depth == 0:
90-
return stand_pat
88+
# Draw detection: fifty-move rule, insufficient material
89+
# Note: Repetition is checked after making moves, not here
90+
if board.is_fifty_moves() or board.is_insufficient_material():
91+
return 0
9192

92-
# beta-cutoff
93-
if stand_pat >= beta:
94-
return beta
93+
stand_pat = self.eval_board(board)
94+
95+
# When in check, we can't use stand-pat for pruning (position is unstable)
96+
# We must search all evasions. However, still respect depth limit.
97+
if in_check:
98+
# In check: search all evasions, but don't use stand-pat for cutoffs
99+
if depth <= 0:
100+
# At depth limit while in check: return evaluation
101+
# (not ideal but prevents infinite recursion)
102+
return stand_pat
103+
104+
best_score = float("-inf")
105+
moves = list(board.legal_moves) # All evasions
106+
else:
107+
# Not in check: normal quiescence behavior
108+
# recursion base case
109+
if depth <= 0:
110+
return stand_pat
111+
112+
# beta-cutoff: position is already good enough
113+
if stand_pat >= beta:
114+
return beta
95115

96-
# alpha update
97-
alpha = max(alpha, stand_pat)
116+
# Use stand-pat as baseline (we can always choose not to capture)
117+
best_score = stand_pat
118+
alpha = max(alpha, stand_pat)
98119

99-
# get moves for quiescence search
100-
moves = organize_moves_quiescence(board)
120+
# Only tactical moves when not in check
121+
moves = organize_moves_quiescence(board)
101122

102123
for move in moves:
103124
# make move and get score
104125
board.push(move)
105-
score = -self.quiescence_search(
106-
board=board,
107-
depth=depth - 1,
108-
alpha=-beta,
109-
beta=-alpha,
110-
)
126+
127+
# Check if this move leads to a repetition (draw)
128+
if board.is_repetition(2):
129+
score: float = 0 # Draw score
130+
else:
131+
score = -self.quiescence_search(
132+
board=board,
133+
depth=depth - 1,
134+
alpha=-beta,
135+
beta=-alpha,
136+
)
137+
111138
board.pop()
112139

140+
if score > best_score:
141+
best_score = score
142+
113143
# beta-cutoff
114144
if score >= beta:
115145
return beta
116146

117147
# alpha-update
118148
alpha = max(alpha, score)
119149

120-
return alpha
150+
return best_score
121151

122152
def negamax(
123153
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)