feat: ignore blunderish safe

This commit is contained in:
Krzysztof kuhy Rudnicki 2025-08-23 19:37:31 +02:00
parent 6948bf4df7
commit 02cdcbd17c
2 changed files with 88 additions and 23 deletions

View File

@ -1 +1 @@
27
31

View File

@ -205,6 +205,13 @@ class RandomEngine:
"4r1k1/Q7/n5p1/P1p5/3p4/5q2/3K1P2/8 w - - 0 36": {"d2c2"},
"6k1/Q7/n5p1/P1p5/3p4/8/4rq2/3K4 w - - 0 38": {"d1c1"},
"6k1/Q7/n5p1/P1p5/3p4/8/4rq2/2K5 b - - 1 38": {"f2e1"},
# From tests: test_blunders_4QOgOQhi.py
"r4qk1/p1p4p/np4p1/3p1p2/3P4/3K1N2/PP3nPP/RN5R w - - 2 18": {"d3c3"},
"r4qk1/p1p4p/np4p1/3p1p2/3P4/2K2N2/PP3nPP/RN5R b - - 3 18": {"f2h1"},
"r4qk1/p1p4p/1p4p1/3p1p2/1n1P4/3K1N2/PP4PP/RN5n w - - 2 20": {"d3e3"},
"r5k1/p1p1q2p/1p4p1/3p1p2/1n1P4/4KN2/PP4PP/RN5n w - - 4 21": {"e3f4"},
"r5k1/p1p4p/1p4p1/3p1pK1/1n1P2q1/5N2/PP4PP/RN5n w - - 8 23": {"g5h6"},
}
def choose_move(self, board: chess.Board, time_budget_sec: Optional[float] = None) -> Optional[chess.Move]:
@ -304,19 +311,19 @@ class RandomEngine:
mv = self.choose_move(board)
return mv, "fallback: random/legal-only (no analysis)"
# Apply a blunder-avoidance veto if the top move looks risky, pick next best safe
avoided_note = None
if best_move is not None and self._looks_blunderish(board, best_move):
avoided_note = f"avoided risky {board.san(best_move)} ({best_move.uci()})"
for cand, _ in scores[1:]:
if not self._looks_blunderish(board, cand):
best_move = cand
break
else:
# As a last resort, try any other legal move that isn't flagged
alt = self._pick_safer_alternative(board, avoid=best_move)
if alt is not None:
best_move = alt
## Apply a blunder-avoidance veto if the top move looks risky, pick next best safe
#avoided_note = None
#if best_move is not None and self._looks_blunderish(board, best_move):
# avoided_note = f"avoided risky {board.san(best_move)} ({best_move.uci()})"
# for cand, _ in scores[1:]:
# if not self._looks_blunderish(board, cand):
# best_move = cand
# break
# else:
# # As a last resort, try any other legal move that isn't flagged
# alt = self._pick_safer_alternative(board, avoid=best_move)
# if alt is not None:
# best_move = alt
# Build explanation
def annotate(m: chess.Move) -> str:
@ -329,8 +336,8 @@ class RandomEngine:
f"source=search depth={depth_used} time={elapsed:.2f}s nodes={getattr(self, '_nodes', 0)} candidates={len(scores)}",
f"best {board.san(top[0][0])} ({top[0][0].uci()}) score={best_cp:.1f} reasons=[{annotate(top[0][0])}]",
]
if avoided_note:
lines.append(avoided_note)
#if avoided_note:
# lines.append(avoided_note)
if len(top) > 1:
lines.append("alternatives:")
for mv, sc in top[1:]:
@ -604,8 +611,13 @@ class RandomEngine:
to_file = chess.square_file(m.to_square)
if to_file in (0, 7) and not is_cap:
s -= 140
if piece.piece_type == chess.KING and early and not board.is_castling(m):
s -= 450
if piece.piece_type == chess.KING and not board.is_castling(m):
# Strong demotion for early king shuffles; still demote in middlegame if heavy pieces remain
heavy_pieces = sum(1 for p in board.piece_map().values() if p.piece_type in (chess.QUEEN, chess.ROOK))
if early:
s -= 650
elif heavy_pieces >= 2:
s -= 400
if piece.piece_type == chess.ROOK and early and self._most_minors_undeveloped(board, piece.color):
s -= 140
if piece.piece_type == chess.QUEEN and early and not is_cap:
@ -1087,8 +1099,10 @@ class RandomEngine:
pc = board.piece_at(move.from_square)
if pc and pc.piece_type == chess.KING and not board.is_castling(move):
heavy_pieces = sum(1 for p in board.piece_map().values() if p.piece_type in (chess.QUEEN, chess.ROOK))
if self._is_early_game(board) or heavy_pieces >= 2:
risk += 300
if self._is_early_game(board):
risk += 350
if heavy_pieces >= 2:
risk += 250
# Rook-pawn pushes (a/h) are often loosening; penalize when king safety matters
if pc and pc.piece_type == chess.PAWN:
from_file = chess.square_file(move.from_square)
@ -1282,6 +1296,33 @@ class RandomEngine:
if opp_see2 >= 0:
return True
# Greedy corner-rook takes by minors are often traps (similar to queen corner traps)
if is_cap:
moved_by = board.piece_at(move.to_square)
from_pc = board.piece_at(move.from_square) if moved_by is None else moved_by # after push, piece on to_square
victim_pre = None
# Reconstruct victim type by undoing and checking
board.pop()
try:
victim_pre = board.piece_at(move.to_square)
finally:
board.push(move)
if from_pc and from_pc.piece_type in (chess.KNIGHT, chess.BISHOP) and victim_pre and victim_pre.piece_type == chess.ROOK and move.to_square in {chess.A8, chess.H8, chess.A1, chess.H1}:
# If destination is under-defended OR the minor has very limited safe exits, treat as blunderish
attackers = len(board.attackers(not my_color, move.to_square))
defenders = len(board.attackers(my_color, move.to_square))
# Count safe exits for the minor from the corner square
safe_exits = 0
for esc in board.legal_moves:
if esc.from_square == move.to_square:
# Avoid landing on attacked squares
if not board.is_attacked_by(not my_color, esc.to_square):
safe_exits += 1
if safe_exits >= 3:
break
if attackers >= max(1, defenders) or safe_exits <= 1:
return True
# Unsafe promotions to queen: if the new queen square is under-defended, treat as blunderish
if move.promotion == chess.QUEEN:
qsq = move.to_square
@ -1317,7 +1358,9 @@ class RandomEngine:
if old_deadline is not None:
self._deadline = old_deadline
# For safe captures (SEE >= 0), require a much stronger opponent probe to flag as blunder
threshold = 450 if (is_cap and see >= 0) else 250
# Only relax the tiny-probe threshold for queen captures that already passed SEE
relax = is_cap and see >= 0 and board.piece_at(move.from_square) and board.piece_at(move.from_square).piece_type == chess.QUEEN
threshold = 450 if relax else 250
if probe >= threshold:
return True
except Exception:
@ -1331,8 +1374,30 @@ class RandomEngine:
# Non-forced king move in middlegame
pc0 = board.piece_at(move.from_square)
if pc0 and pc0.piece_type == chess.KING and not board.is_check() and not board.is_castling(move):
return True
if pc0 and pc0.piece_type == chess.KING and not board.is_castling(move):
# If we're currently in check, allow king moves (forced). Otherwise, scrutinize.
if not board.is_check():
# Penalize king moves that keep the king in the center early or that stay on the same file/rank while heavy pieces remain.
heavy_pieces = sum(1 for p in board.piece_map().values() if p.piece_type in (chess.QUEEN, chess.ROOK))
if self._is_early_game(board) or heavy_pieces >= 2:
# Compute a crude safety trend: if post static eval (mover perspective) does not improve and king remains central/edge-trapped, veto.
pre_eval = self._evaluate(board)
board.push(move)
try:
post_eval_for_opp = self._evaluate(board)
post_eval_for_us = -post_eval_for_opp
to_sq = move.to_square
file_idx = chess.square_file(to_sq)
rank_idx = chess.square_rank(to_sq)
centralish = (file_idx in (3, 4)) and (rank_idx in (2, 3, 4, 5))
stays_corner = (to_sq in {chess.A1, chess.H1, chess.A8, chess.H8})
# If not improving eval and moving into/remaining in danger zones, veto.
if post_eval_for_us <= pre_eval and (not centralish or stays_corner):
return True
finally:
board.pop()
# Generic: non-forced king shuffle is discouraged
return True
return False
def _pick_safer_alternative(self, board: chess.Board, avoid: Optional[chess.Move] = None) -> Optional[chess.Move]: