Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 50 additions & 20 deletions moonfish/engines/alpha_beta.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,47 +91,77 @@ def quiescence_search(
Returns:
- best_score: returns best move's score.
"""
if board.is_stalemate():
return 0
in_check = board.is_check()

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

stand_pat = self.eval_board(board)
if board.is_stalemate():
return 0

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

# beta-cutoff
if stand_pat >= beta:
return beta
stand_pat = self.eval_board(board)

# When in check, we can't use stand-pat for pruning (position is unstable)
# We must search all evasions. However, still respect depth limit.
if in_check:
# In check: search all evasions, but don't use stand-pat for cutoffs
if depth <= 0:
# At depth limit while in check: return evaluation
# (not ideal but prevents infinite recursion)
return stand_pat

best_score = float("-inf")
moves = list(board.legal_moves) # All evasions
else:
# Not in check: normal quiescence behavior
# recursion base case
if depth <= 0:
return stand_pat

# beta-cutoff: position is already good enough
if stand_pat >= beta:
return beta

# alpha update
alpha = max(alpha, stand_pat)
# Use stand-pat as baseline (we can always choose not to capture)
best_score = stand_pat
alpha = max(alpha, stand_pat)

# get moves for quiescence search
moves = organize_moves_quiescence(board)
# Only tactical moves when not in check
moves = organize_moves_quiescence(board)

for move in moves:
# make move and get score
board.push(move)
score = -self.quiescence_search(
board=board,
depth=depth - 1,
alpha=-beta,
beta=-alpha,
)

# Check if this move leads to a repetition (draw)
if board.is_repetition(2):
score = 0.0 # Draw score
else:
score = -self.quiescence_search(
board=board,
depth=depth - 1,
alpha=-beta,
beta=-alpha,
)

board.pop()

if score > best_score:
best_score = score

# beta-cutoff
if score >= beta:
return beta

# alpha-update
alpha = max(alpha, score)

return alpha
return best_score

def negamax(
self,
Expand Down
31 changes: 25 additions & 6 deletions moonfish/move_ordering.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,45 @@ def organize_moves(board: Board):
return captures + non_captures


def is_tactical_move(board: Board, move: Move) -> bool:
"""
Check if a move is tactical (should be searched in quiescence).

Tactical moves are:
- Captures (change material)
- Promotions (significant material gain)
- Moves that give check (forcing)
"""
return (
board.is_capture(move) or move.promotion is not None or board.gives_check(move)
)


def organize_moves_quiescence(board: Board):
"""
This function receives a board and it returns a list of all the
possible moves for the current player, sorted by importance.

Only returns tactical moves: captures, promotions, and checks.

Arguments:
- board: chess board state

Returns:
- moves: list of all the possible moves for the current player sorted based on importance.
- moves: list of tactical moves sorted by importance (MVV-LVA).
"""
phase = get_phase(board)
# filter only important moves for quiescence search
captures = filter(
lambda move: board.is_zeroing(move) or board.gives_check(move),

# Filter only tactical moves for quiescence search
# (captures, promotions, checks - NOT quiet pawn pushes)
tactical_moves = filter(
lambda move: is_tactical_move(board, move),
board.legal_moves,
)
# sort moves by importance

# Sort moves by importance using MVV-LVA
moves = sorted(
captures,
tactical_moves,
key=lambda move: mvv_lva(board, move, phase),
reverse=(board.turn == BLACK),
)
Expand Down