"""Test the engine against Lichess puzzles.""" import csv from pathlib import Path import chess import pytest from python_pkg.lichess_bot.engine import RandomEngine _PUZZLE_CSV = Path(__file__).parent / "lichess_db_puzzle.csv" def _load_top_puzzles(csv_path: str | Path, limit: int = 8) -> list[tuple[str, str]]: """Return a list of (FEN, solution_moves_str) for the first `limit` rows in the CSV. CSV columns: PuzzleId,FEN,Moves,... """ puzzles: list[tuple[str, str]] = [] if not Path(csv_path).is_file(): return puzzles with Path(csv_path).open(newline="", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: fen = row["FEN"].strip() moves = row["Moves"].strip() if fen and moves: puzzles.append((fen, moves)) if len(puzzles) >= limit: break return puzzles _PUZZLES = _load_top_puzzles(_PUZZLE_CSV, limit=8) @pytest.mark.skipif(not _PUZZLES, reason="Puzzle CSV not found") @pytest.mark.parametrize( ("fen", "moves_str"), _PUZZLES or [("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "e2e4")], ) def test_puzzle_engine_follow_solution(fen: str, moves_str: str) -> None: """Verify the engine follows puzzle solutions correctly.""" board = chess.Board(fen) eng = RandomEngine(max_time_sec=1.0) # Moves are space-separated UCIs alternating sides # starting from side-to-move in the FEN solution_moves = moves_str.split() for step, uci in enumerate(solution_moves, start=1): # Engine move on this ply mv, expl = eng.choose_move_with_explanation(board, time_budget_sec=0.5) assert mv is not None, f"No move returned at step {step}.\nExplanation: {expl}" # If engine move differs from solution, fail immediately # but provide analysis of the correct move if mv.uci() != uci: # Ask the engine to analyze the correct move for debug score_cp, proposed_expl, best_mv, best_expl = ( eng.evaluate_proposed_move_with_suggestion( board, uci, time_budget_sec=0.5 ) ) details = [ f"Puzzle failed at step {step}.", f"FEN: {fen}", f"Expected: {uci}", f"Engine played: {mv.uci()}", "--- engine explanation ---", expl, "--- analysis of expected move ---", f"score_cp: {score_cp}", proposed_expl, ] if best_mv is not None: details.append("--- engine best move analysis ---") details.append(best_expl) pytest.fail("\n".join(details)) # Apply the move and continue board.push(mv)