feat: fix the queen blunder

This commit is contained in:
Krzysztof kuhy Rudnicki 2025-08-23 15:46:05 +02:00
parent 1daeee077c
commit 7f02645ca6
3 changed files with 85 additions and 8 deletions

View File

@ -1 +1 @@
14
17

View File

@ -179,7 +179,9 @@ class RandomEngine:
alpha = score
if alpha >= beta:
break
scored.sort(key=lambda t: t[1], reverse=True)
# Prefer higher score; on ties, prefer lower risk
risk_map = {m: self._risk_score(board, m) for m, _ in scored}
scored.sort(key=lambda t: (t[1], -risk_map[t[0]]), reverse=True)
return scored
def _search_root(self, board: chess.Board, depth: int, start: float) -> Tuple[float, Optional[chess.Move]]:
@ -306,6 +308,17 @@ class RandomEngine:
s -= 600
# Discourage premature queen adventures in the opening
if piece.piece_type == chess.QUEEN and early:
# Strongly demote greedy corner rook captures like Qxh8/Qxa8/Qxh1/Qxa1
if is_cap:
victim = board.piece_at(m.to_square)
if victim and victim.piece_type == chess.ROOK and m.to_square in {chess.A8, chess.H8, chess.A1, chess.H1}:
try:
if board.gives_check(m):
s -= 1200
else:
s -= 900
except Exception:
s -= 900
victim = board.piece_at(m.to_square)
# Penalize queen pawn-grabs on edge pawns (a2/b2/g2/h2 or a7/b7/g7/h7)
poison_targets_white = {chess.A7, chess.B7, chess.G7, chess.H7}
@ -706,4 +719,67 @@ class RandomEngine:
# Extra risk for early bishop sac on f2/f7
if self._is_early_game(board) and self._is_bishop_sac_on_f2f7(board, move):
risk += 600
# Queen trap risk (e.g., greedy corner rook grabs like Qxh8?)
try:
risk += self._queen_trap_risk(board, move)
except Exception:
pass
return risk
def _queen_trap_risk(self, board: chess.Board, move: chess.Move) -> int:
"""Estimate risk of the mover's queen becoming trapped or heavily attacked after this move.
Adds a notable penalty for queen captures on corner rooks when defenders outweigh attackers
or when the queen has very limited safe mobility from the destination square.
"""
pc = board.piece_at(move.from_square)
if not pc or pc.piece_type != chess.QUEEN:
return 0
# Pre-move info about target square
victim_pre = board.piece_at(move.to_square)
is_corner = move.to_square in {chess.A8, chess.H8, chess.A1, chess.H1}
is_corner_rook_capture = bool(victim_pre and victim_pre.piece_type == chess.ROOK and is_corner)
# Simulate the move
board.push(move)
try:
my_color = not board.turn # after push, side to move flipped; queen belongs to the previous mover
qsq = move.to_square
risk = 0
# If queen moved to a corner, that's typically risky (limited squares)
if qsq in {chess.A8, chess.H8, chess.A1, chess.H1}:
risk += 120
# Count attackers/defenders on the queen's square
attackers = len(board.attackers(not my_color, qsq))
defenders = len(board.attackers(my_color, qsq))
if attackers >= max(1, defenders):
# Heavily attacked or under-defended queen on destination
risk += 350
# Estimate queen mobility: how many immediate moves are not landing on attacked squares
safe_exits = 0
for m in board.legal_moves:
if m.from_square == qsq:
# Quick static safety: avoid landing on currently attacked squares
if not board.is_attacked_by(not my_color, m.to_square):
safe_exits += 1
if safe_exits >= 4:
break
if safe_exits <= 1:
risk += 450
elif safe_exits <= 3:
risk += 200
# Extra penalty if this was a corner rook capture and exits are limited or square is contested
if is_corner_rook_capture:
base = 300
# If heavily attacked or exits are poor, escalate
if attackers >= max(1, defenders) or safe_exits <= 2:
base += 600
# Taking with check is often tempting; still risky. Keep the penalty significant.
risk += base
finally:
board.pop()
return risk

View File

@ -138,8 +138,11 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
logging.info(
f"Game {game_id}: turn={'white' if is_white_turn else 'black'}, my_turn={my_turn}"
)
# Only move on 'gameState' events; skip making a move on initial 'gameFull'
allow_move = (et == "gameState")
# Move policy:
# - Always move on 'gameState' (authoritative)
# - Also allow moving on the initial 'gameFull' when there are zero moves and it's our turn.
# This avoids stalling at game start when Lichess doesn't immediately send a 'gameState' for 0 moves.
allow_move = (et == "gameState") or (et == "gameFull" and new_len == 0)
if my_turn and allow_move:
# Compute a per-move time budget (seconds) based on remaining time
# Heuristic: use min( max_time_sec, max(0.05, 0.6 * my_time_left/remaining_moves + inc) )
@ -168,10 +171,8 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
api.make_move(game_id, move)
except Exception as e:
logging.warning(f"Game {game_id}: move {move.uci()} failed: {e}")
# Mark this position as handled only on authoritative gameState,
# or after we've actually attempted a move. This prevents missing
# our very first move as White when gameFull (len=0) is followed by
# gameState (len=0).
# Mark this position as handled on authoritative gameState, or after we've
# actually attempted a move (including the first move on gameFull len=0).
if et == "gameState" or (my_turn and allow_move):
last_handled_len = new_len
if status in {"mate", "resign", "stalemate", "timeout", "draw"}: