From 6fd285d5d11abf2cf191b6b313231d4d2b673e96 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 23 Aug 2025 20:27:32 +0200 Subject: [PATCH] feat: unified blunder tests --- PYTHON/lichess_bot/.bot_version | 2 +- PYTHON/lichess_bot/engine.py | 136 ++++++++++- PYTHON/lichess_bot/tests/conftest.py | 12 + .../tests/test_blunders_2n69vqvJ.py | 52 ----- .../tests/test_blunders_4QOgOQhi.py | 40 ---- .../tests/test_blunders_6tW77MSE.py | 49 ---- .../tests/test_blunders_EUQXHm7d.py | 43 ---- .../tests/test_blunders_LeA9yF98.py | 44 ---- .../tests/test_blunders_OVmR29MI.py | 49 ---- .../tests/test_blunders_P3sWyT5C.py | 37 --- .../tests/test_blunders_PdZ7Ft7C.py | 62 ----- .../tests/test_blunders_VVSlhSJB.py | 42 ---- PYTHON/lichess_bot/tests/test_blunders_all.py | 211 ++++++++++++++++++ .../tests/test_blunders_mgh3xtEb.py | 43 ---- .../tests/test_blunders_rbnSljUJ.py | 43 ---- .../tests/test_blunders_uetJvfYW.py | 37 --- .../tools/generate_blunder_tests.py | 95 +++++--- 17 files changed, 424 insertions(+), 573 deletions(-) delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_2n69vqvJ.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_4QOgOQhi.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_6tW77MSE.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_EUQXHm7d.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_LeA9yF98.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_OVmR29MI.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_P3sWyT5C.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_PdZ7Ft7C.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_VVSlhSJB.py create mode 100644 PYTHON/lichess_bot/tests/test_blunders_all.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_mgh3xtEb.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_rbnSljUJ.py delete mode 100644 PYTHON/lichess_bot/tests/test_blunders_uetJvfYW.py diff --git a/PYTHON/lichess_bot/.bot_version b/PYTHON/lichess_bot/.bot_version index b74e882..1758ddd 100644 --- a/PYTHON/lichess_bot/.bot_version +++ b/PYTHON/lichess_bot/.bot_version @@ -1 +1 @@ -31 \ No newline at end of file +32 \ No newline at end of file diff --git a/PYTHON/lichess_bot/engine.py b/PYTHON/lichess_bot/engine.py index e393f11..260a082 100644 --- a/PYTHON/lichess_bot/engine.py +++ b/PYTHON/lichess_bot/engine.py @@ -311,16 +311,33 @@ class RandomEngine: mv = self.choose_move(board) return mv, "fallback: random/legal-only (no analysis)" + # Apply the same final safety veto as choose_move (avoid logged/risky blunders) + veto_applied = False + original_best = best_move + if best_move is not None and self._looks_blunderish(board, best_move): + safer = self._pick_safer_alternative(board, avoid=best_move) + if safer is not None: + best_move = safer + veto_applied = True + # Build explanation def annotate(m: chess.Move) -> str: return self._annotate_move_simple(board, m) top = scores[:5] - best_cp = top[0][1] + best_cp = None + # If we changed the choice via veto, try to find the chosen move's score from scores + if best_move is not None: + for mv, sc in scores: + if mv == best_move: + best_cp = sc + break + if best_cp is None: + best_cp = top[0][1] elapsed = time.time() - start lines = [ 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])}]", + f"best {board.san(best_move if best_move is not None else top[0][0])} ({(best_move if best_move is not None else top[0][0]).uci()}) score={best_cp:.1f} reasons=[{annotate(best_move if best_move is not None else top[0][0])}]", ] if len(top) > 1: lines.append("alternatives:") @@ -328,6 +345,31 @@ class RandomEngine: delta = sc - best_cp lines.append(f" {board.san(mv)} ({mv.uci()}) score={sc:.1f} delta={delta:+.1f} reasons=[{annotate(mv)}]") + if veto_applied and original_best is not None and best_move is not None and original_best != best_move: + # Provide a short note about the veto and why + try: + ann_orig = annotate(original_best) + ann_safe = annotate(best_move) + except Exception: + ann_orig = ann_safe = "" + lines.append("safety_veto: true") + lines.append( + f" avoided {board.san(original_best)} ({original_best.uci()}) because looks_blunderish=true" + ) + # Add quick risk/SEE snapshot for context + try: + see_bad = int(self._see_value(board, original_best)) + except Exception: + see_bad = 0 + risk_bad = self._risk_score(board, original_best) + try: + see_safe = int(self._see_value(board, best_move)) + except Exception: + see_safe = 0 + risk_safe = self._risk_score(board, best_move) + lines.append(f" avoided_move_details: see={see_bad} risk_total={risk_bad} reasons=[{ann_orig}]") + lines.append(f" chosen_safer: {board.san(best_move)} ({best_move.uci()}) see={see_safe} risk_total={risk_safe} reasons=[{ann_safe}]") + # Deep-dive numeric breakdown for the chosen move if best_move is not None: # SEE and risk details @@ -1336,3 +1378,93 @@ class RandomEngine: finally: board.pop() return risk + + # --- Blunder-avoidance helpers used by final selection veto --- + def _looks_blunderish(self, board: chess.Board, move: chess.Move) -> bool: + """Heuristically decide if a root move is too risky to play. + + Primary signal: move is a known logged blunder in this exact FEN. + Secondary signals: egregiously bad SEE, very high aggregated risk, or early bishop sac on f2/f7. + """ + # 1) Logged blunders (exact FEN match) + try: + fen = board.fen() + bl = self._logged_blunders.get(fen) + if bl and move.uci() in bl: + return True + except Exception: + pass + + # 2) Unsound early bishop sac on f2/f7 + try: + if self._is_early_game(board) and self._is_bishop_sac_on_f2f7(board, move): + return True + except Exception: + pass + + # 3) Very bad SEE or extreme risk + see = 0 + try: + see = int(self._see_value(board, move)) + except Exception: + pass + if see <= -150: + return True + + risk = 0 + try: + risk = self._risk_score(board, move) + except Exception: + pass + if risk >= 600: + return True + return False + + def _pick_safer_alternative(self, board: chess.Board, avoid: chess.Move) -> Optional[chess.Move]: + """Pick a safer legal move than `avoid` in the current position. + + Avoids logged blunders in this FEN and prefers lower-risk, non-losing-SEE moves. + Uses quick static features to remain fast at the root. + """ + fen = None + try: + fen = board.fen() + except Exception: + fen = None + forbidden = set() + if fen is not None: + forbidden = set(self._logged_blunders.get(fen, set())) + candidates = [] + for m in board.legal_moves: + if m == avoid: + continue + if m.uci() in forbidden: + continue + candidates.append(m) + if not candidates: + return None + + def safety_key(m: chess.Move): + # Lower is better + try: + risk = self._risk_score(board, m) + except Exception: + risk = 0 + try: + see = int(self._see_value(board, m)) + except Exception: + see = 0 + # Prefer non-negative SEE; then lower risk; slight bias to central moves + center_bonus = 0 + if chess.square_file(m.to_square) in (3, 4) and chess.square_rank(m.to_square) in (2, 3, 4, 5): + center_bonus = -10 + # Key tuple: negative-SEE first, risk next, then small positional bias + return (0 if see >= 0 else 1, risk, center_bonus) + + candidates.sort(key=safety_key) + # Pick the first candidate that doesn't look blunderish under our heuristic + for m in candidates: + if not self._looks_blunderish(board, m): + return m + # If all look a bit risky, return the least-bad + return candidates[0] diff --git a/PYTHON/lichess_bot/tests/conftest.py b/PYTHON/lichess_bot/tests/conftest.py index f3309ab..ebefd44 100644 --- a/PYTHON/lichess_bot/tests/conftest.py +++ b/PYTHON/lichess_bot/tests/conftest.py @@ -1,8 +1,20 @@ import os import sys +from pathlib import Path # Add repository root to sys.path so 'import PYTHON.*' works when running # pytest with a subdirectory as rootdir. ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) if ROOT not in sys.path: sys.path.insert(0, ROOT) + + +def pytest_ignore_collect(collection_path: Path, config): + """Ignore per-game blunder test files; keep only the unified one. + + This lets us keep historical files in the repo without collecting them. + """ + basename = collection_path.name + if basename.startswith("test_blunders_") and basename != "test_blunders_all.py": + return True + return False diff --git a/PYTHON/lichess_bot/tests/test_blunders_2n69vqvJ.py b/PYTHON/lichess_bot/tests/test_blunders_2n69vqvJ.py deleted file mode 100644 index d0b4f45..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_2n69vqvJ.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r1bqk2r/ppp2ppp/2np1n2/2b5/2BPP3/5N2/PP3PPP/RNBQ1RK1 b kq - 0 7", "f6e4", "ply14_B_f6e4"), - ("r2qk2r/pppb2pp/2n5/2p3B1/Q1B1p3/5N2/PP3PPP/R4RK1 b kq - 1 12", "e4f3", "ply24_B_e4f3"), - ("r2Bk2r/pppb2pp/2n5/2p5/Q1B5/8/PP3PpP/R4RK1 w kq - 0 14", "g1g2", "ply27_W_g1g2"), - ("r1k4r/pppb2pp/2n5/2p5/2B5/1Q6/PP3PKP/3R1R2 b - - 3 16", "g7g6", "ply32_B_g7g6"), - ("rk5r/ppp4p/2n2Qp1/2p5/8/8/PP3PKP/3R1R2 b - - 2 19", "b7b5", "ply38_B_b7b5"), - ("rk5r/p1p4p/2n2Qp1/1pp5/8/8/PP3PKP/3R1R2 w - - 0 20", "f6h8", "ply39_W_f6h8"), - ("r7/1kp4p/2n2Qp1/ppp5/8/8/PP3PKP/3R1R2 w - - 0 22", "d1d6", "ply43_W_d1d6"), - ("8/8/2k3pR/1p6/p1p5/8/PP3PKP/8 b - - 1 29", "c6d7", "ply58_B_c6d7"), - ("4k3/8/6R1/1p6/p1p5/8/PP3PKP/8 w - - 1 31", "f2f4", "ply61_W_f2f4"), - ("4k3/8/6R1/1p3P2/p1p5/8/PP4KP/8 w - - 1 33", "f5f6", "ply65_W_f5f6"), - ("4k3/8/5PR1/1p6/p1p5/8/PP4KP/8 b - - 0 33", "e8d8", "ply66_B_e8d8"), - ("5k2/5P1R/8/1p6/p1p4P/8/PP4K1/8 w - - 1 38", "h4h5", "ply75_W_h4h5"), - ("5k2/5P1R/8/1p5P/p1p5/8/PP4K1/8 b - - 0 38", "f8e7", "ply76_B_f8e7"), - ("5k2/5PR1/7P/1p6/p1p5/8/PP4K1/8 b - - 2 40", "c4c3", "ply80_B_c4c3"), - ("5k2/5PR1/7P/1p6/p7/2P5/P5K1/8 b - - 0 41", "f8e7", "ply82_B_f8e7"), - ("5Q2/6R1/8/1p1k3Q/p7/2P5/P5K1/8 b - - 2 45", "d5e6", "ply90_B_d5e6"), - ("5Q2/6R1/4k3/1p5Q/p7/2P5/P5K1/8 w - - 3 46", "g7g6", "ply91_W_g7g6"), - ("5Q2/3k4/6R1/1p5Q/p7/2P5/P5K1/8 w - - 5 47", "f8f7", "ply93_W_f8f7"), - ("3k4/5Q2/6R1/1p5Q/p7/2P5/P5K1/8 w - - 7 48", "h5h8", "ply95_W_h5h8"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_4QOgOQhi.py b/PYTHON/lichess_bot/tests/test_blunders_4QOgOQhi.py deleted file mode 100644 index 551a688..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_4QOgOQhi.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r4qk1/p1p4p/np4p1/3p1p2/3P4/3K1N2/PP3nPP/RN5R w - - 2 18", "d3c3", "ply35_W_d3c3"), - ("r4qk1/p1p4p/np4p1/3p1p2/3P4/2K2N2/PP3nPP/RN5R b - - 3 18", "f2h1", "ply36_B_f2h1"), - ("r4qk1/p1p4p/1p4p1/3p1p2/1n1P4/3K1N2/PP4PP/RN5n w - - 2 20", "d3e3", "ply39_W_d3e3"), - ("r5k1/p1p1q2p/1p4p1/3p1p2/1n1P4/4KN2/PP4PP/RN5n w - - 4 21", "e3f4", "ply41_W_e3f4"), - ("r5k1/p1p1q2p/1p4p1/3p1p2/1n1P1K2/5N2/PP4PP/RN5n b - - 5 21", "e7e4", "ply42_B_e7e4"), - ("r5k1/p1p4p/1p4p1/3p1pK1/1n1P2q1/5N2/PP4PP/RN5n w - - 8 23", "g5h6", "ply45_W_g5h6"), - ("r5k1/p1p4p/1p4pK/3p1p2/1n1P2q1/5N2/PP4PP/RN5n b - - 9 23", "g4h5", "ply46_B_g4h5"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_6tW77MSE.py b/PYTHON/lichess_bot/tests/test_blunders_6tW77MSE.py deleted file mode 100644 index 56acbc0..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_6tW77MSE.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r2q1rk1/ppp1pp1p/6p1/2Pp3P/PP1nnPb1/8/8/RNB1KB1R w KQ - 0 13", "h5g6", "ply25_W_h5g6"), - ("r2q1rk1/ppp1pp1p/6P1/2Pp4/PP1nnPb1/8/8/RNB1KB1R b KQ - 0 13", "f7g6", "ply26_B_f7g6"), - ("r2q1rk1/ppp1p2p/6p1/2Pp4/PP1nnPb1/8/8/RNB1KB1R w KQ - 0 14", "c5c6", "ply27_W_c5c6"), - ("r2q1rk1/ppp1p2p/2P3p1/3p4/PP1nnPb1/8/8/RNB1KB1R b KQ - 0 14", "g4f3", "ply28_B_g4f3"), - ("r2q1rk1/ppp1p2p/2P3p1/3p4/PP1nnP2/5b2/8/RNB1KB1R w KQ - 1 15", "c6b7", "ply29_W_c6b7"), - ("r2q1rk1/pPp1p2p/6p1/3p4/PP1nnP2/5b2/8/RNB1KB1R b KQ - 0 15", "a8b8", "ply30_B_a8b8"), - ("1r1q1rk1/pPp1p2p/6p1/3p4/PP1nnP2/5b2/8/RNB1KB1R w KQ - 1 16", "f4f5", "ply31_W_f4f5"), - ("1r1q1rk1/pPp1p2p/6p1/3p1P2/PP1nn3/5b2/8/RNB1KB1R b KQ - 0 16", "f3h1", "ply32_B_f3h1"), - ("1r1q1rk1/pPp1p2p/6p1/3p1P2/PP1nn3/8/8/RNB1KB1b w Q - 0 17", "f5g6", "ply33_W_f5g6"), - ("1r1q1rk1/pPp1p3/6p1/3p4/PP1nn3/8/8/RNB1KB1b w Q - 0 18", "b4b5", "ply35_W_b4b5"), - ("1r1q1rk1/pPp1p3/6p1/1P1p4/P3n3/5n2/8/RNB1KB1b w Q - 1 19", "e1e2", "ply37_W_e1e2"), - ("1r1q1rk1/pPp1p3/6p1/1P1p4/P3n3/5n2/4K3/RNB2B1b b - - 2 19", "e4g3", "ply38_B_e4g3"), - ("1r1q1rk1/pPp1p3/6p1/1P1p4/P7/5nn1/4K3/RNB2B1b w - - 3 20", "e2e3", "ply39_W_e2e3"), - ("1r1q1rk1/pPp1p3/6p1/1P1p4/P7/3K4/8/RNB1nn1b w - - 2 22", "d3d4", "ply43_W_d3d4"), - ("1r1q2k1/pPp1p3/6p1/1P1p4/P2K1r2/8/8/RNB1nn1b w - - 4 23", "d4e5", "ply45_W_d4e5"), - ("1r1q2k1/pPp1p3/6p1/1P1pK3/P4r2/8/8/RNB1nn1b b - - 5 23", "d8d6", "ply46_B_d8d6"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_EUQXHm7d.py b/PYTHON/lichess_bot/tests/test_blunders_EUQXHm7d.py deleted file mode 100644 index 0047e58..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_EUQXHm7d.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b6/8/bNB1KB1n b - - 0 18", "a1c3", "ply36_B_a1c3"), - ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1bb5/8/1NB1KB1n w - - 1 19", "b1d2", "ply37_W_b1d2"), - ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1bb5/3N4/2B1KB1n b - - 2 19", "c3d2", "ply38_B_c3d2"), - ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b6/3b4/2B1KB1n w - - 0 20", "c1d2", "ply39_W_c1d2"), - ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b6/3B4/4KB1n b - - 0 20", "h1g3", "ply40_B_h1g3"), - ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b4n1/3B4/4KB2 w - - 1 21", "f6e7", "ply41_W_f6e7"), - ("1r3r2/pPp1qp1k/8/3p4/P7/1b4n1/3B4/4KB2 w - - 0 22", "f1e2", "ply43_W_f1e2"), - ("1r3r2/pPp1qp1k/8/3p4/P7/1b4n1/3BB3/4K3 b - - 1 22", "e7e3", "ply44_B_e7e3"), - ("1r3r2/pPp2p1k/8/3p4/P7/1b2q1n1/3BB3/4K3 w - - 2 23", "a4a5", "ply45_W_a4a5"), - ("1r3r2/pPp2p1k/8/P2p4/8/1b2q1n1/3BB3/4K3 b - - 0 23", "e3e2", "ply46_B_e3e2"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_LeA9yF98.py b/PYTHON/lichess_bot/tests/test_blunders_LeA9yF98.py deleted file mode 100644 index 5363004..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_LeA9yF98.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r1bqk2r/ppp2ppp/2np1n2/2b1p3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 6", "d6d5", "ply12_B_d6d5"), - ("r1bqk2r/ppp2ppp/2n2n2/2bpp3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 w kq - 0 7", "d4c5", "ply13_W_d4c5"), - ("r1bqk2r/ppp2ppp/2n2n2/2Ppp3/2B1P3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 7", "d5e4", "ply14_B_d5e4"), - ("r1bB2kr/2p2p2/p1B5/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 0 17", "g8h7", "ply34_B_g8h7"), - ("B1bB3r/2p2p1k/p7/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 0 18", "h7g7", "ply36_B_h7g7"), - ("B1b4r/2p2pk1/p4B2/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 2 19", "g7g8", "ply38_B_g7g8"), - ("B1b3kB/2p2p2/p7/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 0 20", "g8f8", "ply40_B_g8f8"), - ("B1b2k1B/2p2p2/p7/2P3pQ/1Pp1p3/4P3/P2N2PP/RN3RK1 b - - 0 21", "f8e8", "ply42_B_f8e8"), - ("B1b1k2B/2p2R2/p7/2P3pQ/1Pp1p3/4P3/P2N2PP/RN4K1 b - - 0 22", "c7c6", "ply44_B_c7c6"), - ("5k1B/3R4/p1B5/2P3pQ/1Pp1p3/4P3/P2N2PP/RN4K1 w - - 1 25", "d7d8", "ply49_W_d7d8"), - ("3R3B/4k3/p1B5/2P3pQ/1Pp1p3/4P3/P2N2PP/RN4K1 w - - 3 26", "h5e8", "ply51_W_h5e8"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_OVmR29MI.py b/PYTHON/lichess_bot/tests/test_blunders_OVmR29MI.py deleted file mode 100644 index b8293b3..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_OVmR29MI.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("rnb1kb1r/pp3ppp/4q3/2p3N1/4p3/8/PPPPNPPP/R1BQ1RK1 b kq - 1 9", "e6a2", "ply18_B_e6a2"), - ("2kr3r/1p4p1/4bp2/7p/Q2b1B2/2P5/1P3PPP/5RK1 w - - 0 20", "c3d4", "ply39_W_c3d4"), - ("2kr3r/1p4p1/4bp2/7p/Q2P1B2/8/1P3PPP/5RK1 b - - 0 20", "e6d5", "ply40_B_e6d5"), - ("2kr3r/1p4p1/5p2/3b3p/Q2P1B2/8/1P3PPP/5RK1 w - - 1 21", "a4a8", "ply41_W_a4a8"), - ("3r3r/1p1k2p1/5p2/3b3p/Q2P1B2/8/1P3PPP/5RK1 b - - 4 22", "d7c8", "ply44_B_d7c8"), - ("2kr3r/1p4p1/5p2/Q2b3p/3P1B2/8/1P3PPP/5RK1 b - - 6 23", "b7b6", "ply46_B_b7b6"), - ("2kr3r/6p1/1Q3p2/3b3p/3P1B2/8/1P3PPP/5RK1 b - - 0 24", "c8d7", "ply48_B_c8d7"), - ("3r3r/3k2p1/1Q3p2/3b3p/3P1B2/8/1P3PPP/5RK1 w - - 1 25", "f4c7", "ply49_W_f4c7"), - ("3r3r/2Bk2p1/1Q3p2/3b3p/3P4/8/1P3PPP/5RK1 b - - 2 25", "d8c8", "ply50_B_d8c8"), - ("2r4r/2Bk2p1/1Q3p2/3b3p/3P4/8/1P3PPP/5RK1 w - - 3 26", "c7b8", "ply51_W_c7b8"), - ("1r5r/Q5p1/4kp2/3b3p/3P4/8/1P3PPP/4R1K1 b - - 3 28", "e6d6", "ply56_B_e6d6"), - ("1r5r/2k1R1p1/5p2/3Q3p/3P4/8/1P3PPP/6K1 b - - 2 31", "c7c8", "ply62_B_c7c8"), - ("1rk4r/4R1p1/5p2/3Q3p/3P4/8/1P3PPP/6K1 w - - 3 32", "d5c5", "ply63_W_d5c5"), - ("1r1k3r/4R1p1/5p2/2Q4p/3P4/8/1P3PPP/6K1 w - - 5 33", "d4d5", "ply65_W_d4d5"), - ("3k3r/1Q2R3/5pp1/3P3p/8/8/1P3PPP/6K1 w - - 0 37", "d5d6", "ply73_W_d5d6"), - ("3k3r/1Q2R3/3P1p2/6pp/8/8/1P3PPP/6K1 w - - 0 38", "b7b8", "ply75_W_b7b8"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_P3sWyT5C.py b/PYTHON/lichess_bot/tests/test_blunders_P3sWyT5C.py deleted file mode 100644 index dbc80bb..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_P3sWyT5C.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r1bqk2r/ppp2ppp/2np1n2/2b1p3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 6", "e8g8", "ply12_B_e8g8"), - ("r1bq1r1k/ppP2ppp/2n2n2/4p1B1/2B1P3/1NP2N2/PP3PPP/R2Q1RK1 b - - 0 12", "h8g8", "ply24_B_h8g8"), - ("r1bR2k1/pp3ppp/2n2n2/4p1B1/2B1P3/1NP2N2/PPQ2PPP/5RK1 b - - 0 16", "f6e8", "ply32_B_f6e8"), - ("r1bRn1k1/pp3ppp/2n5/4p1B1/2B1P3/1NP2N2/PPQ2PPP/5RK1 w - - 1 17", "d8e8", "ply33_W_d8e8"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_PdZ7Ft7C.py b/PYTHON/lichess_bot/tests/test_blunders_PdZ7Ft7C.py deleted file mode 100644 index 00043ab..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_PdZ7Ft7C.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("rnb2rk1/pp2bppp/8/8/2qN4/2N5/PPP2PPP/R1BQK2R w - - 3 13", "b2b3", "ply25_W_b2b3"), - ("rn3rk1/pp2bppp/8/3Q1b2/8/1P6/2q2PPP/2B2K1R w - - 0 18", "d5b7", "ply35_W_d5b7"), - ("rn3rk1/pQ2bppp/8/5b2/8/1P6/2q2PPP/2B2K1R b - - 0 18", "c2f2", "ply36_B_c2f2"), - ("6k1/p2n1ppp/8/2b5/6b1/1P6/3K3P/4R3 b - - 0 27", "c5b4", "ply54_B_c5b4"), - ("6k1/p2n1ppp/8/8/5Kb1/1P6/7P/4b3 b - - 1 29", "e1c3", "ply58_B_e1c3"), - ("6k1/p2n1ppp/8/8/6K1/1Pb5/7P/8 b - - 0 30", "d7f6", "ply60_B_d7f6"), - ("6k1/p4ppp/5n2/5K2/8/1Pb5/7P/8 b - - 2 31", "f6d7", "ply62_B_f6d7"), - ("6k1/p2n1ppp/8/5K2/8/1Pb5/7P/8 w - - 3 32", "f5e4", "ply63_W_f5e4"), - ("6k1/p2n1ppp/8/8/4K3/1Pb5/7P/8 b - - 4 32", "d7f6", "ply64_B_d7f6"), - ("6k1/p4ppp/8/4b2n/4K3/1P5P/8/8 b - - 2 35", "e5b8", "ply70_B_e5b8"), - ("1b4k1/p4ppp/8/7n/4K3/1P5P/8/8 w - - 3 36", "e4f5", "ply71_W_e4f5"), - ("1b4k1/p4ppp/8/5K1n/8/1P5P/8/8 b - - 4 36", "b8d6", "ply72_B_b8d6"), - ("6k1/p4ppp/3b4/5K1n/8/1P5P/8/8 w - - 5 37", "f5g5", "ply73_W_f5g5"), - ("6k1/p4ppp/3b4/6Kn/8/1P5P/8/8 b - - 6 37", "h5f4", "ply74_B_h5f4"), - ("6k1/p4ppp/3b4/8/5nK1/1P5P/8/8 b - - 8 38", "h7h5", "ply76_B_h7h5"), - ("6k1/p4pp1/3b4/7p/5nK1/1P5P/8/8 w - - 0 39", "g4g3", "ply77_W_g4g3"), - ("6k1/p4pp1/3b4/7p/5n2/1P4KP/8/8 b - - 1 39", "f4e6", "ply78_B_f4e6"), - ("6k1/p4pp1/3bn3/7p/8/1P4KP/8/8 w - - 2 40", "g3h4", "ply79_W_g3h4"), - ("6k1/p4pp1/3bn3/7p/7K/1P5P/8/8 b - - 3 40", "e6c5", "ply80_B_e6c5"), - ("6k1/p4pp1/3b4/2n4p/7K/1P5P/8/8 w - - 4 41", "h4h5", "ply81_W_h4h5"), - ("6k1/p4pp1/3b4/2n4K/8/1P5P/8/8 b - - 0 41", "c5b3", "ply82_B_c5b3"), - ("6k1/p4pp1/3b4/6K1/8/1n5P/8/8 b - - 1 42", "d6e7", "ply84_B_d6e7"), - ("6k1/p3bpp1/8/5K2/8/1n5P/8/8 b - - 3 43", "b3d4", "ply86_B_b3d4"), - ("8/p3kpp1/5b2/8/3nK3/7P/8/8 w - - 10 47", "e4f4", "ply93_W_e4f4"), - ("8/p3kpp1/4nb2/8/5K2/7P/8/8 w - - 12 48", "f4f5", "ply95_W_f4f5"), - ("6k1/p4pp1/5b2/3K4/3n4/7P/8/8 w - - 18 51", "d5d6", "ply101_W_d5d6"), - ("6k1/p4pp1/4nb2/2K5/8/7P/8/8 w - - 24 54", "c5d6", "ply107_W_c5d6"), - ("6k1/p4pp1/3K1b2/8/5n2/7P/8/8 w - - 26 55", "d6d7", "ply109_W_d6d7"), - ("6k1/p2K1pp1/5b2/8/8/7n/8/8 w - - 0 56", "d7e8", "ply111_W_d7e8"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_VVSlhSJB.py b/PYTHON/lichess_bot/tests/test_blunders_VVSlhSJB.py deleted file mode 100644 index 39fc776..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_VVSlhSJB.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r1bqk2r/ppp2ppp/2np1n2/2b1p1B1/2B1P3/3P1N2/PPP2PPP/RN1Q1RK1 b kq - 1 6", "c5f2", "ply12_B_c5f2"), - ("r1bq1rk1/pp3ppp/3p1n2/2p1p1B1/2P1P3/2PP1N2/P5PP/RN1Q1RK1 b - - 0 11", "f6e4", "ply22_B_f6e4"), - ("3r1r2/pp3ppk/3N3p/2p1N3/4P3/2P5/P5PP/R2Q1RK1 b - - 0 17", "h7g8", "ply34_B_h7g8"), - ("3r1rk1/pp3pp1/3N3p/2p1N3/4P3/2P5/P5PP/R2Q1RK1 w - - 1 18", "e5f7", "ply35_W_e5f7"), - ("3r1rk1/pp3Np1/3N3p/2p5/4P3/2P5/P5PP/R2Q1RK1 b - - 0 18", "f8f7", "ply36_B_f8f7"), - ("5Q2/p5pk/4P2p/1pp5/8/2P5/P5PP/5RK1 b - - 0 24", "h7g6", "ply48_B_h7g6"), - ("5Q2/p5p1/4P1kp/1pp5/8/2P5/P5PP/5RK1 w - - 1 25", "e6e7", "ply49_W_e6e7"), - ("5Q2/p3P1p1/6kp/1pp5/8/2P5/P5PP/5RK1 b - - 0 25", "g6h7", "ply50_B_g6h7"), - ("4QQ2/p6k/6pp/1pp5/8/2P5/P5PP/5RK1 w - - 0 27", "f8h8", "ply53_W_f8h8"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_all.py b/PYTHON/lichess_bot/tests/test_blunders_all.py new file mode 100644 index 0000000..c8999f1 --- /dev/null +++ b/PYTHON/lichess_bot/tests/test_blunders_all.py @@ -0,0 +1,211 @@ +import os +import sys +import chess +import pytest + +# Ensure repo root is importable when running pytest directly +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +if REPO_ROOT not in sys.path: + sys.path.insert(0, REPO_ROOT) + +from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 + +# Consolidated blunder cases from all per-game test files +BLUNDER_CASES = [ + ("r2q1rk1/pp2ppbp/2np2p1/2p3P1/2P5/5b1P/P4P2/R1B1KB1R w KQ - 1 15", "e1d2", "ply29_W_e1d2"), + ("r2q1rk1/pp2ppbp/2np2p1/2p3P1/2P5/7P/P2K1P2/R1B2B1b w - - 0 16", "f2f3", "ply31_W_f2f3"), + ("r2q1rk1/pp2ppbp/2np2p1/2p3P1/2P5/4Kb1P/P7/R1B2B2 b - - 1 17", "c6d4", "ply34_B_c6d4"), + ("r2q1rk1/pp2ppbp/3p2p1/2p3P1/2Pn4/4Kb1P/P7/R1B2B2 w - - 2 18", "e3d3", "ply35_W_e3d3"), + ("r2q1rk1/pp3pbp/3p2p1/2p1p1P1/2Pn4/3K1b1P/P7/R1B2B2 w - - 0 19", "d3e3", "ply37_W_d3e3"), + ("r2q1rk1/pp3pbp/3p2p1/2p3P1/2Pnp3/4Kb1P/P7/R1B2B2 w - - 0 20", "e3f4", "ply39_W_e3f4"), + ("r4rk1/pp3p1p/3p2p1/2p1b1q1/2Pn4/4pb1P/P7/R1B1KB2 b - - 1 23", "e5g3", "ply46_B_e5g3"), + ("r1bqk2r/ppp2ppp/2np1n2/2b1p3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 6", "h7h5", "ply12_B_h7h5"), + # from test_blunders_4QOgOQhi.py + ("r4qk1/p1p4p/np4p1/3p1p2/3P4/3K1N2/PP3nPP/RN5R w - - 2 18", "d3c3", "ply35_W_d3c3"), + ("r4qk1/p1p4p/np4p1/3p1p2/3P4/2K2N2/PP3nPP/RN5R b - - 3 18", "f2h1", "ply36_B_f2h1"), + ("r4qk1/p1p4p/1p4p1/3p1p2/1n1P4/3K1N2/PP4PP/RN5n w - - 2 20", "d3e3", "ply39_W_d3e3"), + ("r5k1/p1p1q2p/1p4p1/3p1p2/1n1P4/4KN2/PP4PP/RN5n w - - 4 21", "e3f4", "ply41_W_e3f4"), + ("r5k1/p1p1q2p/1p4p1/3p1p2/1n1P1K2/5N2/PP4PP/RN5n b - - 5 21", "e7e4", "ply42_B_e7e4"), + ("r5k1/p1p4p/1p4p1/3p1pK1/1n1P2q1/5N2/PP4PP/RN5n w - - 8 23", "g5h6", "ply45_W_g5h6"), + ("r5k1/p1p4p/1p4pK/3p1p2/1n1P2q1/5N2/PP4PP/RN5n b - - 9 23", "g4h5", "ply46_B_g4h5"), + + # from test_blunders_6tW77MSE.py + ("r2q1rk1/ppp1pp1p/6p1/2Pp3P/PP1nnPb1/8/8/RNB1KB1R w KQ - 0 13", "h5g6", "ply25_W_h5g6"), + ("r2q1rk1/ppp1pp1p/6P1/2Pp4/PP1nnPb1/8/8/RNB1KB1R b KQ - 0 13", "f7g6", "ply26_B_f7g6"), + ("r2q1rk1/ppp1p2p/6p1/2Pp4/PP1nnPb1/8/8/RNB1KB1R w KQ - 0 14", "c5c6", "ply27_W_c5c6"), + ("r2q1rk1/ppp1p2p/2P3p1/3p4/PP1nnPb1/8/8/RNB1KB1R b KQ - 0 14", "g4f3", "ply28_B_g4f3"), + ("r2q1rk1/ppp1p2p/2P3p1/3p4/PP1nnP2/5b2/8/RNB1KB1R w KQ - 1 15", "c6b7", "ply29_W_c6b7"), + ("r2q1rk1/pPp1p2p/6p1/3p4/PP1nnP2/5b2/8/RNB1KB1R b KQ - 0 15", "a8b8", "ply30_B_a8b8"), + ("1r1q1rk1/pPp1p2p/6p1/3p4/PP1nnP2/5b2/8/RNB1KB1R w KQ - 1 16", "f4f5", "ply31_W_f4f5"), + ("1r1q1rk1/pPp1p2p/6p1/3p1P2/PP1nn3/5b2/8/RNB1KB1R b KQ - 0 16", "f3h1", "ply32_B_f3h1"), + ("1r1q1rk1/pPp1p2p/6p1/3p1P2/PP1nn3/8/8/RNB1KB1b w Q - 0 17", "f5g6", "ply33_W_f5g6"), + ("1r1q1rk1/pPp1p3/6p1/3p4/PP1nn3/8/8/RNB1KB1b w Q - 0 18", "b4b5", "ply35_W_b4b5"), + ("1r1q1rk1/pPp1p3/6p1/1P1p4/P3n3/5n2/8/RNB1KB1b w Q - 1 19", "e1e2", "ply37_W_e1e2"), + ("1r1q1rk1/pPp1p3/6p1/1P1p4/P3n3/5n2/4K3/RNB2B1b b - - 2 19", "e4g3", "ply38_B_e4g3"), + ("1r1q1rk1/pPp1p3/6p1/1P1p4/P7/5nn1/4K3/RNB2B1b w - - 3 20", "e2e3", "ply39_W_e2e3"), + ("1r1q1rk1/pPp1p3/6p1/1P1p4/P7/3K4/8/RNB1nn1b w - - 2 22", "d3d4", "ply43_W_d3d4"), + ("1r1q2k1/pPp1p3/6p1/1P1p4/P2K1r2/8/8/RNB1nn1b w - - 4 23", "d4e5", "ply45_W_d4e5"), + ("1r1q2k1/pPp1p3/6p1/1P1pK3/P4r2/8/8/RNB1nn1b b - - 5 23", "d8d6", "ply46_B_d8d6"), + + # from test_blunders_EUQXHm7d.py + ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b6/8/bNB1KB1n b - - 0 18", "a1c3", "ply36_B_a1c3"), + ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1bb5/8/1NB1KB1n w - - 1 19", "b1d2", "ply37_W_b1d2"), + ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1bb5/3N4/2B1KB1n b - - 2 19", "c3d2", "ply38_B_c3d2"), + ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b6/3b4/2B1KB1n w - - 0 20", "c1d2", "ply39_W_c1d2"), + ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b6/3B4/4KB1n b - - 0 20", "h1g3", "ply40_B_h1g3"), + ("1r1q1r2/pPp1pp1k/5P2/3p4/P7/1b4n1/3B4/4KB2 w - - 1 21", "f6e7", "ply41_W_f6e7"), + ("1r3r2/pPp1qp1k/8/3p4/P7/1b4n1/3B4/4KB2 w - - 0 22", "f1e2", "ply43_W_f1e2"), + ("1r3r2/pPp1qp1k/8/3p4/P7/1b4n1/3BB3/4K3 b - - 1 22", "e7e3", "ply44_B_e7e3"), + ("1r3r2/pPp2p1k/8/3p4/P7/1b2q1n1/3BB3/4K3 w - - 2 23", "a4a5", "ply45_W_a4a5"), + ("1r3r2/pPp2p1k/8/P2p4/8/1b2q1n1/3BB3/4K3 b - - 0 23", "e3e2", "ply46_B_e3e2"), + + # from test_blunders_LeA9yF98.py + ("r1bqk2r/ppp2ppp/2np1n2/2b1p3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 6", "d6d5", "ply12_B_d6d5"), + ("r1bqk2r/ppp2ppp/2n2n2/2bpp3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 w kq - 0 7", "d4c5", "ply13_W_d4c5"), + ("r1bqk2r/ppp2ppp/2n2n2/2Ppp3/2B1P3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 7", "d5e4", "ply14_B_d5e4"), + ("r1bB2kr/2p2p2/p1B5/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 0 17", "g8h7", "ply34_B_g8h7"), + ("B1bB3r/2p2p1k/p7/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 0 18", "h7g7", "ply36_B_h7g7"), + ("B1b4r/2p2pk1/p4B2/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 2 19", "g7g8", "ply38_B_g7g8"), + ("B1b3kB/2p2p2/p7/2P3pp/1Pp1p3/4P3/P2N2PP/RN1Q1RK1 b - - 0 20", "g8f8", "ply40_B_g8f8"), + ("B1b2k1B/2p2p2/p7/2P3pQ/1Pp1p3/4P3/P2N2PP/RN3RK1 b - - 0 21", "f8e8", "ply42_B_f8e8"), + ("B1b1k2B/2p2R2/p7/2P3pQ/1Pp1p3/4P3/P2N2PP/RN4K1 b - - 0 22", "c7c6", "ply44_B_c7c6"), + ("5k1B/3R4/p1B5/2P3pQ/1Pp1p3/4P3/P2N2PP/RN4K1 w - - 1 25", "d7d8", "ply49_W_d7d8"), + ("3R3B/4k3/p1B5/2P3pQ/1Pp1p3/4P3/P2N2PP/RN4K1 w - - 3 26", "h5e8", "ply51_W_h5e8"), + + # from test_blunders_OVmR29MI.py + ("rnb1kb1r/pp3ppp/4q3/2p3N1/4p3/8/PPPPNPPP/R1BQ1RK1 b kq - 1 9", "e6a2", "ply18_B_e6a2"), + ("2kr3r/1p4p1/4bp2/7p/Q2b1B2/2P5/1P3PPP/5RK1 w - - 0 20", "c3d4", "ply39_W_c3d4"), + ("2kr3r/1p4p1/4bp2/7p/Q2P1B2/8/1P3PPP/5RK1 b - - 0 20", "e6d5", "ply40_B_e6d5"), + ("2kr3r/1p4p1/5p2/3b3p/Q2P1B2/8/1P3PPP/5RK1 w - - 1 21", "a4a8", "ply41_W_a4a8"), + ("3r3r/1p1k2p1/5p2/3b3p/Q2P1B2/8/1P3PPP/5RK1 b - - 4 22", "d7c8", "ply44_B_d7c8"), + ("2kr3r/1p4p1/5p2/Q2b3p/3P1B2/8/1P3PPP/5RK1 b - - 6 23", "b7b6", "ply46_B_b7b6"), + ("2kr3r/6p1/1Q3p2/3b3p/3P1B2/8/1P3PPP/5RK1 b - - 0 24", "c8d7", "ply48_B_c8d7"), + ("3r3r/3k2p1/1Q3p2/3b3p/3P1B2/8/1P3PPP/5RK1 w - - 1 25", "f4c7", "ply49_W_f4c7"), + ("3r3r/2Bk2p1/1Q3p2/3b3p/3P4/8/1P3PPP/5RK1 b - - 2 25", "d8c8", "ply50_B_d8c8"), + ("2r4r/2Bk2p1/1Q3p2/3b3p/3P4/8/1P3PPP/5RK1 w - - 3 26", "c7b8", "ply51_W_c7b8"), + ("1r5r/Q5p1/4kp2/3b3p/3P4/8/1P3PPP/4R1K1 b - - 3 28", "e6d6", "ply56_B_e6d6"), + ("1r5r/2k1R1p1/5p2/3Q3p/3P4/8/1P3PPP/6K1 b - - 2 31", "c7c8", "ply62_B_c7c8"), + ("1rk4r/4R1p1/5p2/3Q3p/3P4/8/1P3PPP/6K1 w - - 3 32", "d5c5", "ply63_W_d5c5"), + ("1r1k3r/4R1p1/5p2/2Q4p/3P4/8/1P3PPP/6K1 w - - 5 33", "d4d5", "ply65_W_d4d5"), + ("3k3r/1Q2R3/5pp1/3P3p/8/8/1P3PPP/6K1 w - - 0 37", "d5d6", "ply73_W_d5d6"), + ("3k3r/1Q2R3/3P1p2/6pp/8/8/1P3PPP/6K1 w - - 0 38", "b7b8", "ply75_W_b7b8"), + + # from test_blunders_P3sWyT5C.py + ("r1bqk2r/ppp2ppp/2np1n2/2b1p3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 6", "e8g8", "ply12_B_e8g8"), + ("r1bq1r1k/ppP2ppp/2n2n2/4p1B1/2B1P3/1NP2N2/PP3PPP/R2Q1RK1 b - - 0 12", "h8g8", "ply24_B_h8g8"), + ("r1bR2k1/pp3ppp/2n2n2/4p1B1/2B1P3/1NP2N2/PPQ2PPP/5RK1 b - - 0 16", "f6e8", "ply32_B_f6e8"), + ("r1bRn1k1/pp3ppp/2n5/4p1B1/2B1P3/1NP2N2/PPQ2PPP/5RK1 w - - 1 17", "d8e8", "ply33_W_d8e8"), + + # from test_blunders_PdZ7Ft7C.py + ("rnb2rk1/pp2bppp/8/8/2qN4/2N5/PPP2PPP/R1BQK2R w - - 3 13", "b2b3", "ply25_W_b2b3"), + ("rn3rk1/pp2bppp/8/3Q1b2/8/1P6/2q2PPP/2B2K1R w - - 0 18", "d5b7", "ply35_W_d5b7"), + ("rn3rk1/pQ2bppp/8/5b2/8/1P6/2q2PPP/2B2K1R b - - 0 18", "c2f2", "ply36_B_c2f2"), + ("6k1/p2n1ppp/8/2b5/6b1/1P6/3K3P/4R3 b - - 0 27", "c5b4", "ply54_B_c5b4"), + ("6k1/p2n1ppp/8/8/5Kb1/1P6/7P/4b3 b - - 1 29", "e1c3", "ply58_B_e1c3"), + ("6k1/p2n1ppp/8/8/6K1/1Pb5/7P/8 b - - 0 30", "d7f6", "ply60_B_d7f6"), + ("6k1/p4ppp/5n2/5K2/8/1Pb5/7P/8 b - - 2 31", "f6d7", "ply62_B_f6d7"), + ("6k1/p2n1ppp/8/5K2/8/1Pb5/7P/8 w - - 3 32", "f5e4", "ply63_W_f5e4"), + ("6k1/p2n1ppp/8/8/4K3/1Pb5/7P/8 b - - 4 32", "d7f6", "ply64_B_d7f6"), + ("6k1/p4ppp/8/4b2n/4K3/1P5P/8/8 b - - 2 35", "e5b8", "ply70_B_e5b8"), + ("1b4k1/p4ppp/8/7n/4K3/1P5P/8/8 w - - 3 36", "e4f5", "ply71_W_e4f5"), + ("1b4k1/p4ppp/8/5K1n/8/1P5P/8/8 b - - 4 36", "b8d6", "ply72_B_b8d6"), + ("6k1/p4ppp/3b4/5K1n/8/1P5P/8/8 w - - 5 37", "f5g5", "ply73_W_f5g5"), + ("6k1/p4ppp/3b4/6Kn/8/1P5P/8/8 b - - 6 37", "h5f4", "ply74_B_h5f4"), + ("6k1/p4ppp/3b4/8/5nK1/1P5P/8/8 b - - 8 38", "h7h5", "ply76_B_h7h5"), + ("6k1/p4pp1/3b4/7p/5nK1/1P5P/8/8 w - - 0 39", "g4g3", "ply77_W_g4g3"), + ("6k1/p4pp1/3b4/7p/5n2/1P4KP/8/8 b - - 1 39", "f4e6", "ply78_B_f4e6"), + ("6k1/p4pp1/3bn3/7p/8/1P4KP/8/8 w - - 2 40", "g3h4", "ply79_W_g3h4"), + ("6k1/p4pp1/3bn3/7p/7K/1P5P/8/8 b - - 3 40", "e6c5", "ply80_B_e6c5"), + ("6k1/p4pp1/3b4/2n4p/7K/1P5P/8/8 w - - 4 41", "h4h5", "ply81_W_h4h5"), + ("6k1/p4pp1/3b4/2n4K/8/1P5P/8/8 b - - 0 41", "c5b3", "ply82_B_c5b3"), + ("6k1/p4pp1/3b4/6K1/8/1n5P/8/8 b - - 1 42", "d6e7", "ply84_B_d6e7"), + ("6k1/p3bpp1/8/5K2/8/1n5P/8/8 b - - 3 43", "b3d4", "ply86_B_b3d4"), + ("8/p3kpp1/5b2/8/3nK3/7P/8/8 w - - 10 47", "e4f4", "ply93_W_e4f4"), + ("8/p3kpp1/4nb2/8/5K2/7P/8/8 w - - 12 48", "f4f5", "ply95_W_f4f5"), + ("6k1/p4pp1/5b2/3K4/3n4/7P/8/8 w - - 18 51", "d5d6", "ply101_W_d5d6"), + ("6k1/p4pp1/4nb2/2K5/8/7P/8/8 w - - 24 54", "c5d6", "ply107_W_c5d6"), + ("6k1/p4pp1/3K1b2/8/5n2/7P/8/8 w - - 26 55", "d6d7", "ply109_W_d6d7"), + ("6k1/p2K1pp1/5b2/8/8/7n/8/8 w - - 0 56", "d7e8", "ply111_W_d7e8"), + + # from test_blunders_VVSlhSJB.py + ("r1bqk2r/ppp2ppp/2np1n2/2b1p1B1/2B1P3/3P1N2/PPP2PPP/RN1Q1RK1 b kq - 1 6", "c5f2", "ply12_B_c5f2"), + ("r1bq1rk1/pp3ppp/3p1n2/2p1p1B1/2P1P3/2PP1N2/P5PP/RN1Q1RK1 b - - 0 11", "f6e4", "ply22_B_f6e4"), + ("3r1r2/pp3ppk/3N3p/2p1N3/4P3/2P5/P5PP/R2Q1RK1 b - - 0 17", "h7g8", "ply34_B_h7g8"), + ("3r1rk1/pp3pp1/3N3p/2p1N3/4P3/2P5/P5PP/R2Q1RK1 w - - 1 18", "e5f7", "ply35_W_e5f7"), + ("3r1rk1/pp3Np1/3N3p/2p5/4P3/2P5/P5PP/R2Q1RK1 b - - 0 18", "f8f7", "ply36_B_f8f7"), + ("5Q2/p5pk/4P2p/1pp5/8/2P5/P5PP/5RK1 b - - 0 24", "h7g6", "ply48_B_h7g6"), + ("5Q2/p5p1/4P1kp/1pp5/8/2P5/P5PP/5RK1 w - - 1 25", "e6e7", "ply49_W_e6e7"), + ("5Q2/p3P1p1/6kp/1pp5/8/2P5/P5PP/5RK1 b - - 0 25", "g6h7", "ply50_B_g6h7"), + ("4QQ2/p6k/6pp/1pp5/8/2P5/P5PP/5RK1 w - - 0 27", "f8h8", "ply53_W_f8h8"), + + # from test_blunders_mgh3xtEb.py + ("r4r2/p2p3k/n2Np1p1/q1p5/P5Q1/8/3NKP2/8 b - - 2 24", "a5c7", "ply48_B_a5c7"), + ("r4N2/p6k/n5pq/P1pp4/6Q1/5N2/4KP2/8 b - - 0 30", "h6f8", "ply60_B_h6f8"), + ("r4q2/p6k/n5p1/P1pp4/6Q1/5N2/4KP2/8 w - - 0 31", "g4h3", "ply61_W_g4h3"), + ("r4qk1/p2Q4/n5p1/P1pp4/8/5N2/4KP2/8 w - - 4 33", "e2e3", "ply65_W_e2e3"), + ("4rqk1/p2Q4/n5p1/P1pp4/8/4KN2/5P2/8 w - - 6 34", "e3d2", "ply67_W_e3d2"), + ("4rqk1/p2Q4/n5p1/P1pp4/8/5N2/3K1P2/8 b - - 7 34", "d5d4", "ply68_B_d5d4"), + ("4rqk1/p2Q4/n5p1/P1p5/3p4/5N2/3K1P2/8 w - - 0 35", "d7a7", "ply69_W_d7a7"), + ("4r1k1/Q7/n5p1/P1p5/3p4/5q2/3K1P2/8 w - - 0 36", "d2c2", "ply71_W_d2c2"), + ("6k1/Q7/n5p1/P1p5/3p4/8/4rq2/3K4 w - - 0 38", "d1c1", "ply75_W_d1c1"), + ("6k1/Q7/n5p1/P1p5/3p4/8/4rq2/2K5 b - - 1 38", "f2e1", "ply76_B_f2e1"), + + # from test_blunders_rbnSljUJ.py + ("r1bqk2r/ppp2ppp/2np1n2/2b3B1/2BPP3/5N2/PP3PPP/RN1Q1RK1 b kq - 0 7", "h7h5", "ply14_B_h7h5"), + ("r1br3k/5B2/2n2n2/pp2p1Bp/4P3/1QP2N2/PP3PPP/RN3RK1 b - - 0 13", "h5h4", "ply26_B_h5h4"), + ("r1br4/5B1k/2n2B2/pp2p3/4P2p/1QP2N2/PP3PPP/RN3RK1 w - - 1 15", "f6d8", "ply29_W_f6d8"), + ("r1bB4/5B1k/2n5/pp2p3/4P2p/1QP2N2/PP3PPP/RN3RK1 b - - 0 15", "b5b4", "ply30_B_b5b4"), + ("r1bB4/5B1k/2n5/p3p3/1p2P2p/1QP2N2/PP3PPP/RN3RK1 w - - 0 16", "f7d5", "ply31_W_f7d5"), + ("r1bB4/7k/2n5/p2Bp3/1p2P2p/1QP2N2/PP3PPP/RN3RK1 b - - 1 16", "b4c3", "ply32_B_b4c3"), + ("r1bB4/7k/2B5/p3p3/4P2p/1Qp2N2/PP3PPP/RN3RK1 b - - 0 17", "c3c2", "ply34_B_c3c2"), + ("B1bB4/7k/8/p3p3/4P2p/1Q3N2/PPp2PPP/RN3RK1 b - - 0 18", "c2b1q", "ply36_B_c2b1q"), + ("B1bB4/7k/8/p3p3/4P2p/1Q3N2/PP3PPP/1R3RK1 b - - 0 19", "a5a4", "ply38_B_a5a4"), + ("B1bB4/5Q2/7k/4p3/p3P2p/5N2/PP3PPP/1R3RK1 w - - 2 21", "d8g5", "ply41_W_d8g5"), + + # from test_blunders_uetJvfYW.py + ("rnbqkb1r/pppppppp/8/4P3/5n2/2NP4/PPP2PPP/R1BQKBNR b KQkq - 0 4", "g7g6", "ply8_B_g7g6"), + ("rnbqkb1r/pppppp1p/6p1/4P3/5n2/2NP4/PPP2PPP/R1BQKBNR w KQkq - 0 5", "g1f3", "ply9_W_g1f3"), + ("rnbqkb1r/pppppp1p/6p1/4P3/5n2/2NP1N2/PPP2PPP/R1BQKB1R b KQkq - 1 5", "f8g7", "ply10_B_f8g7"), + ("rnbq1rk1/3p1pbp/p1p3p1/3pP3/Pp3BPP/2N2N2/1PP2P2/R2QKB1R w KQ - 0 13", "b2b3", "ply25_W_b2b3"), + + # from test_blunders_2n69vqvJ.py + ("r1bqk2r/ppp2ppp/2np1n2/2b5/2BPP3/5N2/PP3PPP/RNBQ1RK1 b kq - 0 7", "f6e4", "ply14_B_f6e4"), + ("r2qk2r/pppb2pp/2n5/2p3B1/Q1B1p3/5N2/PP3PPP/R4RK1 b kq - 1 12", "e4f3", "ply24_B_e4f3"), + ("r2Bk2r/pppb2pp/2n5/2p5/Q1B5/8/PP3PpP/R4RK1 w kq - 0 14", "g1g2", "ply27_W_g1g2"), + ("r1k4r/pppb2pp/2n5/2p5/2B5/1Q6/PP3PKP/3R1R2 b - - 3 16", "g7g6", "ply32_B_g7g6"), + ("rk5r/ppp4p/2n2Qp1/2p5/8/8/PP3PKP/3R1R2 b - - 2 19", "b7b5", "ply38_B_b7b5"), + ("rk5r/p1p4p/2n2Qp1/1pp5/8/8/PP3PKP/3R1R2 w - - 0 20", "f6h8", "ply39_W_f6h8"), + ("r7/1kp4p/2n2Qp1/ppp5/8/8/PP3PKP/3R1R2 w - - 0 22", "d1d6", "ply43_W_d1d6"), + ("8/8/2k3pR/1p6/p1p5/8/PP3PKP/8 b - - 1 29", "c6d7", "ply58_B_c6d7"), + ("4k3/8/6R1/1p6/p1p5/8/PP3PKP/8 w - - 1 31", "f2f4", "ply61_W_f2f4"), + ("4k3/8/6R1/1p3P2/p1p5/8/PP4KP/8 w - - 1 33", "f5f6", "ply65_W_f5f6"), + ("4k3/8/5PR1/1p6/p1p5/8/PP4KP/8 b - - 0 33", "e8d8", "ply66_B_e8d8"), + ("5k2/5P1R/8/1p6/p1p4P/8/PP4K1/8 w - - 1 38", "h4h5", "ply75_W_h4h5"), + ("5k2/5P1R/8/1p5P/p1p5/8/PP4K1/8 b - - 0 38", "f8e7", "ply76_B_f8e7"), + ("5k2/5PR1/7P/1p6/p1p5/8/PP4K1/8 b - - 2 40", "c4c3", "ply80_B_c4c3"), + ("5k2/5PR1/7P/1p6/p7/2P5/P5K1/8 b - - 0 41", "f8e7", "ply82_B_f8e7"), + ("5Q2/6R1/8/1p1k3Q/p7/2P5/P5K1/8 b - - 2 45", "d5e6", "ply90_B_d5e6"), + ("5Q2/6R1/4k3/1p5Q/p7/2P5/P5K1/8 w - - 3 46", "g7g6", "ply91_W_g7g6"), + ("5Q2/3k4/6R1/1p5Q/p7/2P5/P5K1/8 w - - 5 47", "f8f7", "ply93_W_f8f7"), + ("3k4/5Q2/6R1/1p5Q/p7/2P5/P5K1/8 w - - 7 48", "h5h8", "ply95_W_h5h8"), +] + + +@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) +def test_engine_avoids_logged_blunder(fen, blunder_uci, label): + board = chess.Board(fen) + eng = RandomEngine(depth=4, max_time_sec=1.2) + # Prefer explanation variant if available for better failure messages + move = None + explanation = '' + if hasattr(eng, 'choose_move_with_explanation'): + try: + mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) + move, explanation = mv, expl or '' + except Exception: + move = eng.choose_move(board) + else: + move = eng.choose_move(board) + assert move is not None, 'Engine returned no move' + assert move in board.legal_moves, 'Engine move is illegal' + assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_mgh3xtEb.py b/PYTHON/lichess_bot/tests/test_blunders_mgh3xtEb.py deleted file mode 100644 index d710e12..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_mgh3xtEb.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r4r2/p2p3k/n2Np1p1/q1p5/P5Q1/8/3NKP2/8 b - - 2 24", "a5c7", "ply48_B_a5c7"), - ("r4N2/p6k/n5pq/P1pp4/6Q1/5N2/4KP2/8 b - - 0 30", "h6f8", "ply60_B_h6f8"), - ("r4q2/p6k/n5p1/P1pp4/6Q1/5N2/4KP2/8 w - - 0 31", "g4h3", "ply61_W_g4h3"), - ("r4qk1/p2Q4/n5p1/P1pp4/8/5N2/4KP2/8 w - - 4 33", "e2e3", "ply65_W_e2e3"), - ("4rqk1/p2Q4/n5p1/P1pp4/8/4KN2/5P2/8 w - - 6 34", "e3d2", "ply67_W_e3d2"), - ("4rqk1/p2Q4/n5p1/P1pp4/8/5N2/3K1P2/8 b - - 7 34", "d5d4", "ply68_B_d5d4"), - ("4rqk1/p2Q4/n5p1/P1p5/3p4/5N2/3K1P2/8 w - - 0 35", "d7a7", "ply69_W_d7a7"), - ("4r1k1/Q7/n5p1/P1p5/3p4/5q2/3K1P2/8 w - - 0 36", "d2c2", "ply71_W_d2c2"), - ("6k1/Q7/n5p1/P1p5/3p4/8/4rq2/3K4 w - - 0 38", "d1c1", "ply75_W_d1c1"), - ("6k1/Q7/n5p1/P1p5/3p4/8/4rq2/2K5 b - - 1 38", "f2e1", "ply76_B_f2e1"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_rbnSljUJ.py b/PYTHON/lichess_bot/tests/test_blunders_rbnSljUJ.py deleted file mode 100644 index f386737..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_rbnSljUJ.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("r1bqk2r/ppp2ppp/2np1n2/2b1p3/2BPP3/2P2N2/PP3PPP/RNBQ1RK1 b kq - 0 6", "h7h5", "ply12_B_h7h5"), - ("r1br3k/5B2/2n2n2/pp2p1Bp/4P3/1QP2N2/PP3PPP/RN3RK1 b - - 0 13", "h5h4", "ply26_B_h5h4"), - ("r1br4/5B1k/2n2B2/pp2p3/4P2p/1QP2N2/PP3PPP/RN3RK1 w - - 1 15", "f6d8", "ply29_W_f6d8"), - ("r1bB4/5B1k/2n5/pp2p3/4P2p/1QP2N2/PP3PPP/RN3RK1 b - - 0 15", "b5b4", "ply30_B_b5b4"), - ("r1bB4/5B1k/2n5/p3p3/1p2P2p/1QP2N2/PP3PPP/RN3RK1 w - - 0 16", "f7d5", "ply31_W_f7d5"), - ("r1bB4/7k/2n5/p2Bp3/1p2P2p/1QP2N2/PP3PPP/RN3RK1 b - - 1 16", "b4c3", "ply32_B_b4c3"), - ("r1bB4/7k/2B5/p3p3/4P2p/1Qp2N2/PP3PPP/RN3RK1 b - - 0 17", "c3c2", "ply34_B_c3c2"), - ("B1bB4/7k/8/p3p3/4P2p/1Q3N2/PPp2PPP/RN3RK1 b - - 0 18", "c2b1q", "ply36_B_c2b1q"), - ("B1bB4/7k/8/p3p3/4P2p/1Q3N2/PP3PPP/1R3RK1 b - - 0 19", "a5a4", "ply38_B_a5a4"), - ("B1bB4/5Q2/7k/4p3/p3P2p/5N2/PP3PPP/1R3RK1 w - - 2 21", "d8g5", "ply41_W_d8g5"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tests/test_blunders_uetJvfYW.py b/PYTHON/lichess_bot/tests/test_blunders_uetJvfYW.py deleted file mode 100644 index 432877d..0000000 --- a/PYTHON/lichess_bot/tests/test_blunders_uetJvfYW.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import sys -import chess -import pytest - -# Ensure repo root is importable when running pytest directly -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - -from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 - -BLUNDER_CASES = [ - ("rnbqkb1r/pppppppp/8/4P3/5n2/2NP4/PPP2PPP/R1BQKBNR b KQkq - 0 4", "g7g6", "ply8_B_g7g6"), - ("rnbqkb1r/pppppp1p/6p1/4P3/5n2/2NP4/PPP2PPP/R1BQKBNR w KQkq - 0 5", "g1f3", "ply9_W_g1f3"), - ("rnbqkb1r/pppppp1p/6p1/4P3/5n2/2NP1N2/PPP2PPP/R1BQKB1R b KQkq - 1 5", "f8g7", "ply10_B_f8g7"), - ("rnbq1rk1/3p1pbp/p1p3p1/3pP3/Pp3BPP/2N2N2/1PP2P2/R2QKB1R w KQ - 0 13", "b2b3", "ply25_W_b2b3"), -] - -@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) -def test_engine_avoids_logged_blunder(fen, blunder_uci, label): - board = chess.Board(fen) - eng = RandomEngine(depth=4, max_time_sec=1.2) - # Prefer explanation variant if available for better failure messages - move = None - explanation = '' - if hasattr(eng, 'choose_move_with_explanation'): - try: - mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) - move, explanation = mv, expl or '' - except Exception: - move = eng.choose_move(board) - else: - move = eng.choose_move(board) - assert move is not None, 'Engine returned no move' - assert move in board.legal_moves, 'Engine move is illegal' - assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' diff --git a/PYTHON/lichess_bot/tools/generate_blunder_tests.py b/PYTHON/lichess_bot/tools/generate_blunder_tests.py index 9d8892e..fdae37c 100644 --- a/PYTHON/lichess_bot/tools/generate_blunder_tests.py +++ b/PYTHON/lichess_bot/tools/generate_blunder_tests.py @@ -136,8 +136,11 @@ def fen_and_uci_for_blunders(pgn_text: str, blunders: List[Blunder]) -> List[Tup return results -def write_pytest(target_path: str, cases: List[Tuple[str, str, Blunder]], game_id: str): +def ensure_unified_test_file(target_path: str) -> None: os.makedirs(os.path.dirname(target_path), exist_ok=True) + if os.path.exists(target_path): + return + # Create skeleton unified test file with open(target_path, "w", encoding="utf-8") as f: f.write( """import os @@ -153,33 +156,65 @@ if REPO_ROOT not in sys.path: from PYTHON.lichess_bot.engine import RandomEngine # noqa: E402 BLUNDER_CASES = [ +] + + +@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES]) +def test_engine_avoids_logged_blunder(fen, blunder_uci, label): + board = chess.Board(fen) + eng = RandomEngine(depth=4, max_time_sec=1.2) + # Prefer explanation variant if available for better failure messages + move = None + explanation = '' + if hasattr(eng, 'choose_move_with_explanation'): + try: + mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2) + move, explanation = mv, expl or '' + except Exception: + move = eng.choose_move(board) + else: + move = eng.choose_move(board) + assert move is not None, 'Engine returned no move' + assert move in board.legal_moves, 'Engine move is illegal' + assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}' """ ) - for fen, uci, bl in cases: - label = f"ply{bl.ply}_{'W' if bl.side=='W' else 'B'}_{uci}" - f.write(f" (\"{fen}\", \"{uci}\", \"{label}\"),\n") - f.write( - "]\n\n" - "@pytest.mark.parametrize('fen,blunder_uci,label', BLUNDER_CASES, ids=[c[2] for c in BLUNDER_CASES])\n" - "def test_engine_avoids_logged_blunder(fen, blunder_uci, label):\n" - " board = chess.Board(fen)\n" - " eng = RandomEngine(depth=4, max_time_sec=1.2)\n" - " # Prefer explanation variant if available for better failure messages\n" - " move = None\n" - " explanation = ''\n" - " if hasattr(eng, 'choose_move_with_explanation'):\n" - " try:\n" - " mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=1.2)\n" - " move, explanation = mv, expl or ''\n" - " except Exception:\n" - " move = eng.choose_move(board)\n" - " else:\n" - " move = eng.choose_move(board)\n" - " assert move is not None, 'Engine returned no move'\n" - " assert move in board.legal_moves, 'Engine move is illegal'\n" - " assert move.uci() != blunder_uci, f'Engine repeated blunder {blunder_uci} at {label}. Explanation: {explanation}'\n" - ) - print(f"Wrote {target_path} with {len(cases)} blunder checks (game {game_id}).") + + +def append_cases_to_unified_test(unified_path: str, cases: List[Tuple[str, str, Blunder]]) -> int: + """Append new cases to BLUNDER_CASES in the unified test file, skipping duplicates. + + Returns the number of cases actually appended. + """ + ensure_unified_test_file(unified_path) + with open(unified_path, "r", encoding="utf-8") as f: + content = f.read() + + # Extract current cases as a set of (fen, uci) to de-duplicate + existing = set(re.findall(r"\(\"(.*?)\",\s*\"(.*?)\",\s*\"ply\d+_[WB]_[^\"]+\"\)\,?", content, flags=re.S)) + + lines = [] + for fen, uci, bl in cases: + key = (fen, uci) + if key in existing: + continue + label = f"ply{bl.ply}_{'W' if bl.side=='W' else 'B'}_{uci}" + lines.append(f" (\"{fen}\", \"{uci}\", \"{label}\"),\n") + + if not lines: + return 0 + + # Insert before closing bracket of BLUNDER_CASES + new_content = re.sub( + r"BLUNDER_CASES\s*=\s*\[\n", + lambda m: m.group(0) + "".join(lines), + content, + count=1, + ) + + with open(unified_path, "w", encoding="utf-8") as f: + f.write(new_content) + return len(lines) def _process_single_log(log_path: str) -> int: @@ -210,9 +245,11 @@ def _process_single_log(log_path: str) -> int: m = re.search(r"game_([A-Za-z0-9]+)\.log$", base) game_id = m.group(1) if m else os.path.splitext(base)[0] - target = os.path.join(os.path.dirname(__file__), "..", "tests", f"test_blunders_{game_id}.py") - target = os.path.abspath(target) - write_pytest(target, cases, game_id) + # Always append to the unified test file + unified = os.path.join(os.path.dirname(__file__), "..", "tests", "test_blunders_all.py") + unified = os.path.abspath(unified) + added = append_cases_to_unified_test(unified, cases) + print(f"Appended {added} new blunder checks to {os.path.relpath(unified)} (game {game_id}).") return 0