testsAndMisc/PYTHON/lichess_bot/tests/test_puzzles.py
Krzysztof kuhy Rudnicki 87cbcd5ff6 fix(lint): remove style preference rules from global ignores
Fixed violations:
- PERF401: Use list comprehension (downloadCats, split)
- SIM102: Combine nested if statements (keyboardCoop)
- SIM113: Use enumerate (lichess_bot/tests)
- B904: Add 'from None/e' to raises (engine, stockfish_analysis, generate_blunder_tests)
- TRY300: Move return to else block (screen_locker)

Per-file ignores added for TRY301:
- lichess_bot/main.py: Null checks on subprocess streams
- randomize_numbers/random_digits.py: Input validation

Removed from global ignore (no violations):
- RUF005, SIM103, SIM105, SIM108

Global ignore now only contains unavoidable conflicts:
- D203/D213: Docstring style conflicts
- COM812/ISC001: Formatter conflicts (per ruff docs)
2025-11-30 20:47:38 +01:00

76 lines
2.5 KiB
Python

"""Test the engine against Lichess puzzles."""
import csv
import os
import chess
import pytest
from PYTHON.lichess_bot.engine import RandomEngine
def _load_top_puzzles(csv_path: str, 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]] = []
with open(csv_path, 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
@pytest.mark.parametrize(
("fen", "moves_str"),
_load_top_puzzles(
os.path.join(os.path.dirname(__file__), "lichess_db_puzzle.csv"), limit=8
),
)
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)