"""Test lichess-bot.""" import yaml import chess import chess.engine import threading import os import sys import datetime import time import logging import tempfile from multiprocessing import Manager from queue import Queue import test_bot.lichess from lib import config from lib.timer import Timer, seconds from lib.engine_wrapper import test_suffix from lib.lichess_types import CONFIG_DICT_TYPE if "pytest" not in sys.modules: sys.exit(f"The script {os.path.basename(__file__)} should only be run by pytest.") from lib import lichess_bot from test_bot.test_games import scholars_mate logging_level = logging.DEBUG testing_log_file_name = None lichess_bot.logging_configurer(logging_level, testing_log_file_name, True) logger = logging.getLogger(__name__) def lichess_org_simulator(move_queue: Queue[chess.Move | None], board_queue: Queue[chess.Board], clock_queue: Queue[tuple[datetime.timedelta, datetime.timedelta, datetime.timedelta]], results: Queue[bool]) -> None: """ Run a mocked version of the lichess.org server to provide an opponent for a test. This opponent always plays white. :param opponent_path: The path to the executable of the opponent. Usually Stockfish. :param move_queue: An interprocess queue that supplies the moves chosen by the bot being tested. :param board_queue: An interprocess queue where this function sends the updated board after choosing a move. :param clock_queue: An interprocess queue where this function sends the updated game clock after choosing a move. :param results: An interprocess queue where this function sends the result of the game to the testing function. """ start_time = seconds(10) increment = seconds(0.1) board = chess.Board() wtime = start_time btime = start_time while not board.is_game_over(): move_timer = Timer() if board.turn == chess.WHITE: move_count = len(board.move_stack) engine_move = board.parse_uci(scholars_mate[move_count]) board.push(engine_move) board_queue.put(board) clock_queue.put((wtime, btime, increment)) if len(board.move_stack) > 1: wtime -= move_timer.time_since_reset() wtime += increment else: move_timer = Timer() while (bot_move := move_queue.get()) is None: board_queue.put(board) clock_queue.put((wtime, btime, increment)) move_queue.task_done() board.push(bot_move) move_queue.task_done() if len(board.move_stack) > 2: btime -= move_timer.time_since_reset() btime += increment board_queue.put(board) clock_queue.put((wtime, btime, increment)) outcome = board.outcome() results.put(outcome is not None and outcome.winner == chess.BLACK) def run_bot(raw_config: CONFIG_DICT_TYPE, logging_level: int) -> bool: """ Start lichess-bot test with a mocked version of the lichess.org site. :param raw_config: A dictionary of values to specify the engine to test. This engine will play as white. :param logging_level: The level of logging to use during the test. Usually logging.DEBUG. :param opponent_path: The path to the executable that will play the opponent. The opponent plays as black. """ config.insert_default_values(raw_config) CONFIG = config.Configuration(raw_config) logger.info(lichess_bot.intro()) manager = Manager() board_queue: Queue[chess.Board] = manager.Queue() clock_queue: Queue[tuple[datetime.timedelta, datetime.timedelta, datetime.timedelta]] = manager.Queue() move_queue: Queue[chess.Move | None] = manager.Queue() li = test_bot.lichess.Lichess(move_queue, board_queue, clock_queue) user_profile = li.get_profile() username = user_profile["username"] if user_profile.get("title") != "BOT": return False logger.info(f"Welcome {username}!") lichess_bot.disable_restart() results: Queue[bool] = manager.Queue() thr = threading.Thread(target=lichess_org_simulator, args=[move_queue, board_queue, clock_queue, results]) thr.start() lichess_bot.start(li, user_profile, CONFIG, logging_level, testing_log_file_name, True, one_game=True) result = results.get() results.task_done() results.join() board_queue.join() clock_queue.join() move_queue.join() thr.join() return result def test_uci() -> None: """Test lichess-bot with Stockfish (UCI).""" with open("./config.yml.default") as file: CONFIG = yaml.safe_load(file) with tempfile.TemporaryDirectory() as temp: CONFIG["token"] = "" CONFIG["engine"]["dir"] = "test_bot" CONFIG["engine"]["name"] = "uci_engine.py" CONFIG["engine"]["interpreter"] = sys.executable CONFIG["pgn_directory"] = os.path.join(temp, "uci_game_record") CONFIG["engine"]["uci_options"] = {} win = run_bot(CONFIG, logging_level) logger.info("Finished Testing UCI") assert win time.sleep(0.1) # Wait for file to be written. assert os.path.isfile(os.path.join(CONFIG["pgn_directory"], "bo vs b - zzzzzzzz.pgn")) def test_xboard() -> None: """Test lichess-bot with an XBoard engine.""" with open("./config.yml.default") as file: CONFIG = yaml.safe_load(file) with tempfile.TemporaryDirectory() as temp: CONFIG["token"] = "" CONFIG["engine"]["dir"] = "test_bot" CONFIG["engine"]["name"] = "xboard_engine.py" CONFIG["engine"]["protocol"] = "xboard" CONFIG["engine"]["interpreter"] = sys.executable CONFIG["pgn_directory"] = os.path.join(temp, "lc0_game_record") win = run_bot(CONFIG, logging_level) logger.info("Finished Testing XBoard") assert win time.sleep(0.1) # Wait for file to be written. assert os.path.isfile(os.path.join(CONFIG["pgn_directory"], "bo vs b - zzzzzzzz.pgn")) def test_homemade() -> None: """Test lichess-bot with a homemade engine.""" with open("./config.yml.default") as file: CONFIG = yaml.safe_load(file) with tempfile.TemporaryDirectory() as temp: CONFIG["token"] = "" CONFIG["engine"]["name"] = f"ScholarsMate{test_suffix}" CONFIG["engine"]["protocol"] = "homemade" CONFIG["pgn_directory"] = os.path.join(temp, "homemade_game_record") win = run_bot(CONFIG, logging_level) logger.info("Finished Testing Homemade") assert win time.sleep(0.1) # Wait for file to be written. assert os.path.isfile(os.path.join(CONFIG["pgn_directory"], "bo vs b - zzzzzzzz.pgn")) def test_buggy_engine() -> None: """Test lichess-bot with an engine that causes a timeout error within python-chess.""" with open("./config.yml.default") as file: CONFIG = yaml.safe_load(file) with tempfile.TemporaryDirectory() as temp: CONFIG["token"] = "" CONFIG["engine"]["dir"] = "test_bot" CONFIG["engine"]["name"] = "buggy_engine.py" CONFIG["engine"]["interpreter"] = sys.executable CONFIG["engine"]["uci_options"] = {"go_commands": {"movetime": 100}} CONFIG["pgn_directory"] = os.path.join(temp, "bug_game_record") win = run_bot(CONFIG, logging_level) logger.info("Finished Testing Buggy Engine") assert win time.sleep(0.1) # Wait for file to be written. assert os.path.isfile(os.path.join(CONFIG["pgn_directory"], "bo vs b - zzzzzzzz.pgn"))