VeloCT_Base / test_bot /test_external_moves.py
Taperx's picture
Deploy Base model with clean official lichess-bot folder
3014f14
Raw
History Blame Contribute Delete
15.1 kB
"""Test the functions that get the external moves."""
import pytest
import backoff
import requests
import yaml
import os
import chess
import logging
import chess.engine
from datetime import timedelta
from copy import deepcopy
from requests.exceptions import ConnectionError as RequestsConnectionError, HTTPError, ReadTimeout, RequestException
from http.client import RemoteDisconnected
from lib.lichess_types import OnlineType, GameEventType
from typing import cast
from lib.lichess import is_final, backoff_handler, Lichess
from lib.config import Configuration, insert_default_values
from lib.model import Game
from lib.engine_wrapper import get_online_move, get_book_move, is_op1_position
class MockLichess(Lichess):
"""A modified Lichess class for communication with external move sources."""
def __init__(self) -> None:
"""Initialize only self.other_session and not self.session."""
self.max_retries = 3
self.other_session = requests.Session()
def online_book_get(self, path: str, params: dict[str, str | int] | None = None, *,
stream: bool = False, authenticated: bool = False) -> OnlineType:
"""
Get an external move from online sources (chessdb or lichess.org).
Ignore authentication for tests.
"""
del authenticated
@backoff.on_exception(backoff.constant,
(RemoteDisconnected, RequestsConnectionError, HTTPError, ReadTimeout),
max_time=60,
max_tries=self.max_retries,
interval=0.1,
giveup=is_final,
on_backoff=backoff_handler,
backoff_log_level=logging.DEBUG,
giveup_log_level=logging.DEBUG)
def online_book_get() -> OnlineType:
json_response: OnlineType = self.other_session.get(path, timeout=2, params=params, stream=stream).json()
return json_response
return online_book_get()
def is_website_up(self, url: str) -> bool:
"""Check if a website is up."""
try:
self.other_session.get(url, timeout=2)
return True
except RequestException:
return False
def get_configs() -> tuple[Configuration, Configuration, Configuration, Configuration]:
"""Create the configs used for the tests."""
with open("./config.yml.default") as file:
CONFIG = yaml.safe_load(file)
insert_default_values(CONFIG)
CONFIG["engine"]["online_moves"]["lichess_cloud_analysis"]["enabled"] = True
CONFIG["engine"]["online_moves"]["online_egtb"]["enabled"] = True
CONFIG["engine"]["draw_or_resign"]["resign_enabled"] = True
CONFIG["engine"]["polyglot"]["enabled"] = True
CONFIG["engine"]["polyglot"]["book"]["standard"] = ["TEMP/gm2001.bin"]
engine_cfg = Configuration(CONFIG).engine
CONFIG_2 = deepcopy(CONFIG)
CONFIG_2["engine"]["online_moves"]["chessdb_book"]["enabled"] = True
CONFIG_2["engine"]["online_moves"]["online_egtb"]["source"] = "chessdb"
engine_cfg_2 = Configuration(CONFIG_2).engine
return engine_cfg.online_moves, engine_cfg_2.online_moves, engine_cfg.draw_or_resign, engine_cfg.polyglot
def get_game() -> Game:
"""Create a model.Game to be used in the tests."""
game_event: GameEventType = {"id": "zzzzzzzz",
"variant": {"key": "standard",
"name": "Standard",
"short": "Std"},
"clock": {"initial": 60000,
"increment": 2000},
"speed": "bullet",
"perf": {"name": "Bullet"},
"rated": True,
"createdAt": 1600000000000,
"white": {"id": "bo",
"name": "bo",
"title": "BOT",
"rating": 3000},
"black": {"id": "b",
"name": "b",
"title": "BOT",
"rating": 3000,
"provisional": True},
"initialFen": "startpos",
"type": "gameFull",
"state": {"type": "gameState",
"moves": "",
"wtime": 1000000,
"btime": 1000000,
"winc": 2000,
"binc": 2000,
"status": "started"}}
return Game(game_event, "b", "https://lichess.org", timedelta(seconds=60))
def download_opening_book() -> None:
"""Download gm2001.bin."""
if os.path.exists("./TEMP/gm2001.bin"):
return
os.makedirs("TEMP", exist_ok=True)
response = requests.get("https://github.com/gmcheems-org/free-opening-books/raw/main/books/bin/gm2001.bin",
allow_redirects=True, timeout=60)
if response.status_code != 200:
pytest.xfail("Could not download opening book.")
with open("./TEMP/gm2001.bin", "wb") as file:
file.write(response.content)
def get_online_move_wrapper(li: Lichess, board: chess.Board, game: Game, online_moves_cfg: Configuration,
draw_or_resign_cfg: Configuration, *, expect_none: bool = False) -> chess.engine.PlayResult:
"""Wrap `lib.engine_wrapper.get_online_move` so that it only returns a PlayResult type."""
online_move = get_online_move(li, board, game, online_moves_cfg, draw_or_resign_cfg)
online_move = cast(chess.engine.PlayResult, online_move)
if not expect_none and online_move.move is None:
pytest.xfail("Could not contact external move source.")
return online_move
class TestExternalMoves:
"""Test that the code for external moves works properly."""
li = MockLichess()
game = get_game()
online_cfg, online_cfg_2, draw_or_resign_cfg, polyglot_cfg = get_configs()
starting_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
opening_fen = "rn1q1rk1/pbp1bpp1/1p2pn1p/3p4/2PP3B/2N1PN2/PP2BPPP/R2QK2R w KQ - 2 9"
middlegame_fen = "8/5p2/1n1p1nk1/1p1Pp1p1/1Pp1P1Pp/r1P2B1P/2RNKP2/8 w - - 0 31"
endgame_wdl2_fen = "2k5/4n2Q/5N2/8/8/8/1r6/2K5 b - - 0 123"
endgame_wdl1_fen = "6N1/3n4/3k1b2/8/8/7Q/1r6/5K2 b - - 6 9"
endgame_wdl0_fen = "6N1/3n4/3k1b2/8/8/7Q/5K2/1r6 b - - 8 10"
endgame_op1_fen = "r7/p1b1k3/8/8/P7/8/8/K2R1R2 w - - 1 2"
def test_lichess_cloud_analysis(self) -> None:
"""Test lichess_cloud_analysis."""
if not self.li.is_website_up("https://lichess.org/api/cloud-eval"):
pytest.xfail("Lichess cloud eval is down.")
assert get_online_move_wrapper(self.li,
chess.Board(self.starting_fen),
self.game,
self.online_cfg,
self.draw_or_resign_cfg).move is not None
assert get_online_move_wrapper(self.li,
chess.Board(self.opening_fen),
self.game,
self.online_cfg,
self.draw_or_resign_cfg).move is not None
assert get_online_move_wrapper(self.li,
chess.Board(self.middlegame_fen),
self.game,
self.online_cfg,
self.draw_or_resign_cfg,
expect_none=True).move is None
def test_chessdb_book(self) -> None:
"""Test chessdb_book."""
if not self.li.is_website_up("https://www.chessdb.cn/cdb.php"):
pytest.xfail("ChessDB is down.")
assert get_online_move_wrapper(self.li,
chess.Board(self.starting_fen),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg).move is not None
assert get_online_move_wrapper(self.li,
chess.Board(self.opening_fen),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg).move is not None
assert get_online_move_wrapper(self.li,
chess.Board(self.middlegame_fen),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg,
expect_none=True).move is None
def test_online_egtb_with_lichess(self) -> None:
"""Test online_egtb with lichess."""
if not self.li.is_website_up("https://tablebase.lichess.ovh/standard"):
pytest.xfail("Lichess tablebase is down.")
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl2_fen),
self.game,
self.online_cfg,
self.draw_or_resign_cfg).resigned
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl0_fen),
self.game,
self.online_cfg,
self.draw_or_resign_cfg).draw_offered
wdl1_move = get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl1_fen),
self.game,
self.online_cfg,
self.draw_or_resign_cfg)
assert not wdl1_move.resigned and not wdl1_move.draw_offered
op1_move = get_online_move_wrapper(self.li,
chess.Board(self.endgame_op1_fen),
self.game,
self.online_cfg,
self.draw_or_resign_cfg)
assert op1_move.move == chess.Move.from_uci("f1e1")
# Test with reversed colors.
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl2_fen).mirror(),
self.game,
self.online_cfg,
self.draw_or_resign_cfg).resigned
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl0_fen).mirror(),
self.game,
self.online_cfg,
self.draw_or_resign_cfg).draw_offered
wdl1_move = get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl1_fen).mirror(),
self.game,
self.online_cfg,
self.draw_or_resign_cfg)
assert not wdl1_move.resigned and not wdl1_move.draw_offered
def test_online_egtb_with_chessdb(self) -> None:
"""Test online_egtb with chessdb."""
if not self.li.is_website_up("https://www.chessdb.cn/cdb.php"):
pytest.xfail("ChessDB is down.")
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl2_fen),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg).resigned
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl0_fen),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg).draw_offered
wdl1_move = get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl1_fen),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg)
assert not wdl1_move.resigned and not wdl1_move.draw_offered
# Test with reversed colors.
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl2_fen).mirror(),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg).resigned
assert get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl0_fen).mirror(),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg).draw_offered
wdl1_move = get_online_move_wrapper(self.li,
chess.Board(self.endgame_wdl1_fen).mirror(),
self.game,
self.online_cfg_2,
self.draw_or_resign_cfg)
assert not wdl1_move.resigned and not wdl1_move.draw_offered
def test_opening_book(self) -> None:
"""Test opening book."""
download_opening_book()
assert get_book_move(chess.Board(self.opening_fen), self.game, self.polyglot_cfg).move == chess.Move.from_uci("h4f6")
class TestExternalMoveHelpers:
"""Test the helper functions for external moves."""
def test_op1_positions(self) -> None:
"""Test that a position is op1."""
fens_op1 = ["r7/p1b1k3/8/8/P7/8/8/K2R1R2 w - - 1 2", "3qk3/1n1pp3/8/8/8/8/4P2P/4K3 w - - 0 1"]
fens_not_op1 = ["r3k3/2b5/8/p7/P7/8/8/K2R1R2 w q - 0 2", "3qk3/1n1pp1n1/8/8/8/8/4P3/4K3 w - - 0 1"]
for fen in fens_op1:
assert is_op1_position(chess.Board(fen))
for fen in fens_not_op1:
assert not is_op1_position(chess.Board(fen))