From 15b6e4a098631debd494b6f3c15ea86f879623f1 Mon Sep 17 00:00:00 2001 From: "Ioannis \"Giannis\" Pantidis" <40605232+AttackingOrDefending@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:29:24 +0100 Subject: [PATCH 1/4] Fix some ruff errors and simplify LICHESS_TYPE --- lib/conversation.py | 7 +--- lib/engine_wrapper.py | 69 ++++++++++++++------------------- lib/lichess.py | 14 +++---- lib/lichess_bot.py | 48 +++++++++++------------ lib/matchmaking.py | 17 ++++---- lib/model.py | 15 +++---- lib/timer.py | 7 ++-- test_bot/lichess.py | 19 +++++---- test_bot/ruff.toml | 7 +--- test_bot/test_external_moves.py | 8 ++-- 10 files changed, 94 insertions(+), 117 deletions(-) diff --git a/lib/conversation.py b/lib/conversation.py index 2ad21d8f1..3b79f12b1 100644 --- a/lib/conversation.py +++ b/lib/conversation.py @@ -1,15 +1,12 @@ """Allows lichess-bot to send messages to the chat.""" import logging -import test_bot.lichess from lib import model from lib.engine_wrapper import EngineWrapper -from lib import lichess +from lib.lichess import Lichess from lib.lichess_types import GameEventType from collections.abc import Sequence from lib.timer import seconds -from typing import Union MULTIPROCESSING_LIST_TYPE = Sequence[model.Challenge] -LICHESS_TYPE = Union[lichess.Lichess, test_bot.lichess.Lichess] logger = logging.getLogger(__name__) @@ -30,7 +27,7 @@ def __init__(self, message_info: GameEventType) -> None: class Conversation: """Enables the bot to communicate with its opponent and the spectators.""" - def __init__(self, game: model.Game, engine: EngineWrapper, li: LICHESS_TYPE, version: str, + def __init__(self, game: model.Game, engine: EngineWrapper, li: Lichess, version: str, challenge_queue: MULTIPROCESSING_LIST_TYPE) -> None: """ Communication between lichess-bot and the game chats. diff --git a/lib/engine_wrapper.py b/lib/engine_wrapper.py index 948383770..aadb8d130 100644 --- a/lib/engine_wrapper.py +++ b/lib/engine_wrapper.py @@ -13,7 +13,6 @@ import random import math import contextlib -import test_bot.lichess from collections import Counter from collections.abc import Callable from lib import model, lichess @@ -26,7 +25,6 @@ from operator import itemgetter from typing import Any, Optional, Union, Literal, cast from types import TracebackType -LICHESS_TYPE = Union[lichess.Lichess, test_bot.lichess.Lichess] logger = logging.getLogger(__name__) @@ -132,7 +130,7 @@ def __exit__(self, exc_type: Optional[type[BaseException]], def play_move(self, board: chess.Board, game: model.Game, - li: LICHESS_TYPE, + li: lichess.Lichess, setup_timer: Timer, move_overhead: datetime.timedelta, can_ponder: bool, @@ -193,8 +191,7 @@ def play_move(self, game_ender = li.abort if game.is_abortable() else li.resign game_ender(game.id) return - else: - raise + raise # Heed min_time elapsed = setup_timer.time_since_reset() @@ -283,8 +280,7 @@ def comment_index(self, move_stack_index: int) -> int: """ if self.comment_start_index < 0: return -1 - else: - return move_stack_index - self.comment_start_index + return move_stack_index - self.comment_start_index def comment_for_board_index(self, index: int) -> InfoStrDict: """ @@ -355,16 +351,15 @@ def readable_time(self, number: int) -> str: minutes, seconds = divmod(number, 60) if minutes >= 1: return f"{minutes:0.0f}m {seconds:0.1f}s" - else: - return f"{seconds:0.1f}s" + return f"{seconds:0.1f}s" def readable_number(self, number: int) -> str: """Convert number to a more human-readable format. e.g. 123456789 -> 123M.""" if number >= 1e9: return f"{round(number / 1e9, 1)}B" - elif number >= 1e6: + if number >= 1e6: return f"{round(number / 1e6, 1)}M" - elif number >= 1e3: + if number >= 1e3: return f"{round(number / 1e3, 1)}K" return str(number) @@ -657,10 +652,9 @@ def move_time(board: chess.Board, """ if len(board.move_stack) < 2: return first_move_time(game), False # No pondering after the first move since a new clock starts afterwards. - elif is_correspondence: + if is_correspondence: return single_move_time(board, game, correspondence_move_time, setup_timer, move_overhead), can_ponder - else: - return game_clock_time(board, game, setup_timer, move_overhead), can_ponder + return game_clock_time(board, game, setup_timer, move_overhead), can_ponder def wbtime(board: chess.Board) -> Literal["wtime", "btime"]: @@ -776,7 +770,7 @@ def get_book_move(board: chess.Board, game: model.Game, return no_book_move -def get_online_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, online_moves_cfg: Configuration, +def get_online_move(li: lichess.Lichess, board: chess.Board, game: model.Game, online_moves_cfg: Configuration, draw_or_resign_cfg: Configuration) -> Union[chess.engine.PlayResult, list[chess.Move]]: """ Get a move from an online source. @@ -828,7 +822,7 @@ def get_online_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, onli return chess.engine.PlayResult(None, None) -def get_chessdb_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, +def get_chessdb_move(li: lichess.Lichess, board: chess.Board, game: model.Game, chessdb_cfg: Configuration) -> tuple[Optional[str], chess.engine.InfoDict]: """Get a move from chessdb.cn's opening book.""" use_chessdb = chessdb_cfg.enabled @@ -865,7 +859,7 @@ def get_chessdb_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, return move, comment -def get_lichess_cloud_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, +def get_lichess_cloud_move(li: lichess.Lichess, board: chess.Board, game: model.Game, lichess_cloud_cfg: Configuration) -> tuple[Optional[str], chess.engine.InfoDict]: """Get a move from the lichess's cloud analysis.""" side = wbtime(board) @@ -917,7 +911,7 @@ def get_lichess_cloud_move(li: LICHESS_TYPE, board: chess.Board, game: model.Gam return move, comment -def get_opening_explorer_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, +def get_opening_explorer_move(li: lichess.Lichess, board: chess.Board, game: model.Game, opening_explorer_cfg: Configuration ) -> tuple[Optional[str], chess.engine.InfoDict]: """Get a move from lichess's opening explorer.""" @@ -968,7 +962,7 @@ def get_opening_explorer_move(li: LICHESS_TYPE, board: chess.Board, game: model. return move, comment -def get_online_egtb_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, online_egtb_cfg: Configuration +def get_online_egtb_move(li: lichess.Lichess, board: chess.Board, game: model.Game, online_egtb_cfg: Configuration ) -> tuple[Union[str, list[str], None], int, chess.engine.InfoDict]: """ Get a move from an online egtb (either by lichess or chessdb). @@ -997,7 +991,7 @@ def get_online_egtb_move(li: LICHESS_TYPE, board: chess.Board, game: model.Game, with contextlib.suppress(Exception): if source == "lichess": return get_lichess_egtb_move(li, game, board, quality, variant) - elif source == "chessdb": + if source == "chessdb": return get_chessdb_egtb_move(li, game, board, quality) return None, -3, {} @@ -1032,7 +1026,7 @@ def get_egtb_move(board: chess.Board, game: model.Game, lichess_bot_tbs: Configu return chess.engine.PlayResult(None, None) -def get_lichess_egtb_move(li: LICHESS_TYPE, game: model.Game, board: chess.Board, quality: str, +def get_lichess_egtb_move(li: lichess.Lichess, game: model.Game, board: chess.Board, quality: str, variant: str) -> tuple[Union[str, list[str], None], int, chess.engine.InfoDict]: """ Get a move from lichess's egtb. @@ -1071,22 +1065,20 @@ def good_enough(possible_move: LichessEGTBMoveType) -> bool: wdl = best_wdl logger.info(f"Suggesting moves from tablebase.lichess.ovh (wdl: {wdl}) for game {game.id}") return move_list, wdl, {"string": "lichess-bot-source:Lichess EGTB"} - else: - best_move = possible_moves[0] - move = best_move["uci"] - wdl = name_to_wld[best_move["category"]] * -1 - dtz = best_move["dtz"] * -1 - dtm = best_move["dtm"] - if dtm: - dtm *= -1 - logger.info(f"Got move {move} from tablebase.lichess.ovh (wdl: {wdl}, dtz: {dtz}, dtm: {dtm})" - f" for game {game.id}") + best_move = possible_moves[0] + move = best_move["uci"] + wdl = name_to_wld[best_move["category"]] * -1 + dtz = best_move["dtz"] * -1 + dtm = best_move["dtm"] + if dtm: + dtm *= -1 + logger.info(f"Got move {move} from tablebase.lichess.ovh (wdl: {wdl}, dtz: {dtz}, dtm: {dtm}) for game {game.id}") return move, wdl, {"string": "lichess-bot-source:Lichess EGTB"} return None, -3, {} -def get_chessdb_egtb_move(li: LICHESS_TYPE, game: model.Game, board: chess.Board, +def get_chessdb_egtb_move(li: lichess.Lichess, game: model.Game, board: chess.Board, quality: str) -> tuple[Union[str, list[str], None], int, chess.engine.InfoDict]: """ Get a move from chessdb's egtb. @@ -1127,13 +1119,12 @@ def good_enough(move: ChessDBMoveType) -> bool: move_list = [move["uci"] for move in possible_moves] logger.info(f"Suggesting moves from from chessdb.cn (wdl: {wdl}) for game {game.id}") return move_list, wdl, {"string": "lichess-bot-source:ChessDB EGTB"} - else: - best_move = possible_moves[0] - score = best_move["score"] - move = best_move["uci"] - wdl = score_to_wdl(score) - dtz = score_to_dtz(score) - logger.info(f"Got move {move} from chessdb.cn (wdl: {wdl}, dtz: {dtz}) for game {game.id}") + best_move = possible_moves[0] + score = best_move["score"] + move = best_move["uci"] + wdl = score_to_wdl(score) + dtz = score_to_dtz(score) + logger.info(f"Got move {move} from chessdb.cn (wdl: {wdl}, dtz: {dtz}) for game {game.id}") return move, wdl, {"string": "lichess-bot-source:ChessDB EGTB"} return None, -3, {} diff --git a/lib/lichess.py b/lib/lichess.py index 896a4aef1..84aa72c56 100644 --- a/lib/lichess.py +++ b/lib/lichess.py @@ -2,7 +2,7 @@ import json import requests from urllib.parse import urljoin -from requests.exceptions import ConnectionError, HTTPError, ReadTimeout +from requests.exceptions import ConnectionError as RequestsConnectionError, HTTPError, ReadTimeout from http.client import RemoteDisconnected import backoff import logging @@ -45,7 +45,7 @@ MAX_CHAT_MESSAGE_LEN = 140 # The maximum characters in a chat message. -class RateLimited(RuntimeError): +class RateLimitedError(RuntimeError): """Exception raised when we are rate limited (status code 429).""" @@ -108,7 +108,7 @@ def __init__(self, token: str, url: str, version: str, logging_level: int, max_r f"The current token has: {scopes}.") @backoff.on_exception(backoff.constant, - (RemoteDisconnected, ConnectionError, HTTPError, ReadTimeout), + (RemoteDisconnected, RequestsConnectionError, HTTPError, ReadTimeout), max_time=60, interval=0.1, giveup=is_final, @@ -184,7 +184,7 @@ def api_get_raw(self, endpoint_name: str, *template_args: str, return response.text @backoff.on_exception(backoff.constant, - (RemoteDisconnected, ConnectionError, HTTPError, ReadTimeout), + (RemoteDisconnected, RequestsConnectionError, HTTPError, ReadTimeout), max_time=60, interval=0.1, giveup=is_final, @@ -234,8 +234,8 @@ def get_path_template(self, endpoint_name: str) -> str: """ path_template = ENDPOINTS[endpoint_name] if self.is_rate_limited(path_template): - raise RateLimited(f"{path_template} is rate-limited. " - f"Will retry in {sec_str(self.rate_limit_time_left(path_template))} seconds.") + raise RateLimitedError(f"{path_template} is rate-limited. " + f"Will retry in {sec_str(self.rate_limit_time_left(path_template))} seconds.") return path_template def set_rate_limit_delay(self, path_template: str, delay_time: datetime.timedelta) -> None: @@ -374,7 +374,7 @@ def online_book_get(self, path: str, params: Optional[dict[str, Union[str, int]] stream: bool = False) -> OnlineType: """Get an external move from online sources (chessdb or lichess.org).""" @backoff.on_exception(backoff.constant, - (RemoteDisconnected, ConnectionError, HTTPError, ReadTimeout), + (RemoteDisconnected, RequestsConnectionError, HTTPError, ReadTimeout), max_time=60, max_tries=self.max_retries, interval=0.1, diff --git a/lib/lichess_bot.py b/lib/lichess_bot.py index 7a24e0ff1..ed628fba6 100644 --- a/lib/lichess_bot.py +++ b/lib/lichess_bot.py @@ -24,13 +24,12 @@ import platform import importlib.metadata import contextlib -import test_bot.lichess from lib.config import load_config, Configuration, log_config from lib.conversation import Conversation, ChatLine from lib.timer import Timer, seconds, msec, hours, to_seconds from lib.lichess_types import (UserProfileType, EventType, GameType, GameEventType, CONTROL_QUEUE_TYPE, CORRESPONDENCE_QUEUE_TYPE, LOGGING_QUEUE_TYPE, PGN_QUEUE_TYPE) -from requests.exceptions import ChunkedEncodingError, ConnectionError, HTTPError, ReadTimeout +from requests.exceptions import ChunkedEncodingError, ConnectionError as RequestsConnectionError, HTTPError, ReadTimeout from rich.logging import RichHandler from collections import defaultdict from collections.abc import Iterator, MutableSequence @@ -41,14 +40,13 @@ from typing import Optional, Union, TypedDict, cast from types import FrameType MULTIPROCESSING_LIST_TYPE = MutableSequence[model.Challenge] -LICHESS_TYPE = Union[lichess.Lichess, test_bot.lichess.Lichess] POOL_TYPE = Pool class PlayGameArgsType(TypedDict, total=False): """Type hint for `play_game_args`.""" - li: LICHESS_TYPE + li: lichess.Lichess control_queue: CONTROL_QUEUE_TYPE user_profile: UserProfileType config: Configuration @@ -109,7 +107,7 @@ def signal_handler(signal: int, frame: Optional[FrameType]) -> None: # noqa: AR signal.signal(signal.SIGINT, signal_handler) -def upgrade_account(li: LICHESS_TYPE) -> bool: +def upgrade_account(li: lichess.Lichess) -> bool: """Upgrade the account to a BOT account.""" try: li.upgrade_to_bot_account() @@ -121,7 +119,7 @@ def upgrade_account(li: LICHESS_TYPE) -> bool: return True -def watch_control_stream(control_queue: CONTROL_QUEUE_TYPE, li: LICHESS_TYPE) -> None: +def watch_control_stream(control_queue: CONTROL_QUEUE_TYPE, li: lichess.Lichess) -> None: """Put the events in a queue.""" error = None while not terminated: @@ -250,7 +248,7 @@ def thread_logging_configurer(queue: LOGGING_QUEUE_TYPE) -> None: root.setLevel(logging.DEBUG) -def start(li: LICHESS_TYPE, user_profile: UserProfileType, config: Configuration, logging_level: int, +def start(li: lichess.Lichess, user_profile: UserProfileType, config: Configuration, logging_level: int, log_filename: Optional[str], disable_auto_logging: bool, one_game: bool = False) -> None: """ Start lichess-bot. @@ -326,7 +324,7 @@ def log_proc_count(change: str, active_games: set[str]) -> None: logger.info(f"{symbol} Process {change}. Count: {len(active_games)}. IDs: {active_games or None}") -def lichess_bot_main(li: LICHESS_TYPE, +def lichess_bot_main(li: lichess.Lichess, user_profile: UserProfileType, config: Configuration, challenge_queue: MULTIPROCESSING_LIST_TYPE, @@ -389,7 +387,8 @@ def lichess_bot_main(li: LICHESS_TYPE, logger.debug(f"Terminating exception:\n{event['error']}") control_queue.task_done() break - elif event["type"] == "local_game_done": + + if event["type"] == "local_game_done": active_games.discard(event["game"]["id"]) matchmaker.game_done() log_proc_count("Freed", active_games) @@ -493,7 +492,7 @@ def start_low_time_games(low_time_games: list[GameType], active_games: set[str], start_game_thread(active_games, game_id, play_game_args, pool) -def accept_challenges(li: LICHESS_TYPE, challenge_queue: MULTIPROCESSING_LIST_TYPE, active_games: set[str], +def accept_challenges(li: lichess.Lichess, challenge_queue: MULTIPROCESSING_LIST_TYPE, active_games: set[str], max_games: int) -> None: """Accept a challenge.""" while len(active_games) < max_games and challenge_queue: @@ -511,7 +510,7 @@ def accept_challenges(li: LICHESS_TYPE, challenge_queue: MULTIPROCESSING_LIST_TY logger.info(f"Skip missing {chlng}") -def check_online_status(li: LICHESS_TYPE, user_profile: UserProfileType, last_check_online_time: Timer) -> None: +def check_online_status(li: lichess.Lichess, user_profile: UserProfileType, last_check_online_time: Timer) -> None: """Check if lichess.org thinks the bot is online or not. If it isn't, we restart it.""" global restart @@ -541,7 +540,7 @@ def sort_challenges(challenge_queue: MULTIPROCESSING_LIST_TYPE, challenge_config challenge_queue[:] = challenge_list -def game_is_active(li: LICHESS_TYPE, game_id: str) -> bool: +def game_is_active(li: lichess.Lichess, game_id: str) -> bool: """Determine if a game is still being played.""" return game_id in (ongoing_game["gameId"] for ongoing_game in li.get_ongoing_games()) @@ -608,7 +607,7 @@ def enough_time_to_queue(event: EventType, config: Configuration) -> bool: return not game["isMyTurn"] or game.get("secondsLeft", math.inf) > minimum_time -def handle_challenge(event: EventType, li: LICHESS_TYPE, challenge_queue: MULTIPROCESSING_LIST_TYPE, +def handle_challenge(event: EventType, li: lichess.Lichess, challenge_queue: MULTIPROCESSING_LIST_TYPE, challenge_config: Configuration, user_profile: UserProfileType, recent_bot_challenges: defaultdict[str, list[Timer]]) -> None: """Handle incoming challenges. It either accepts, declines, or queues them to accept later.""" @@ -631,7 +630,7 @@ def handle_challenge(event: EventType, li: LICHESS_TYPE, challenge_queue: MULTIP @backoff.on_exception(backoff.expo, BaseException, max_time=600, giveup=lichess.is_final, # type: ignore[arg-type] on_backoff=lichess.backoff_handler) -def play_game(li: LICHESS_TYPE, +def play_game(li: lichess.Lichess, game_id: str, control_queue: CONTROL_QUEUE_TYPE, user_profile: UserProfileType, @@ -745,7 +744,8 @@ def play_game(li: LICHESS_TYPE, prior_game = copy.deepcopy(game) elif u_type == "ping" and should_exit_game(board, game, prior_game, li, is_correspondence): stay_in_game = False - except (HTTPError, ReadTimeout, RemoteDisconnected, ChunkedEncodingError, ConnectionError, StopIteration) as e: + except (HTTPError, ReadTimeout, RemoteDisconnected, ChunkedEncodingError, RequestsConnectionError, + StopIteration) as e: stopped = isinstance(e, StopIteration) stay_in_game = not stopped and (move_attempted or game_is_active(li, game.id)) @@ -875,24 +875,23 @@ def is_game_over(game: model.Game) -> bool: return status != "started" -def should_exit_game(board: chess.Board, game: model.Game, prior_game: Optional[model.Game], li: LICHESS_TYPE, +def should_exit_game(board: chess.Board, game: model.Game, prior_game: Optional[model.Game], li: lichess.Lichess, is_correspondence: bool) -> bool: """Whether we should exit a game.""" if (is_correspondence and not is_engine_move(game, prior_game, board) and game.should_disconnect_now()): return True - elif game.should_abort_now(): + if game.should_abort_now(): logger.info(f"Aborting {game.url()} by lack of activity") li.abort(game.id) return True - elif game.should_terminate_now(): + if game.should_terminate_now(): logger.info(f"Terminating {game.url()} by lack of activity") if game.is_abortable(): li.abort(game.id) return True - else: - return False + return False def final_queue_entries(control_queue: CONTROL_QUEUE_TYPE, correspondence_queue: CORRESPONDENCE_QUEUE_TYPE, @@ -964,7 +963,7 @@ def tell_user_game_result(game: model.Game, board: chess.Board) -> None: logger.info(f"Game ended by {termination}") -def try_get_pgn_game_record(li: LICHESS_TYPE, config: Configuration, game: model.Game, board: chess.Board, +def try_get_pgn_game_record(li: lichess.Lichess, config: Configuration, game: model.Game, board: chess.Board, engine: engine_wrapper.EngineWrapper) -> str: """ Call `print_pgn_game_record` to write the game to a PGN file and handle errors raised by it. @@ -982,7 +981,7 @@ def try_get_pgn_game_record(li: LICHESS_TYPE, config: Configuration, game: model return "" -def pgn_game_record(li: LICHESS_TYPE, config: Configuration, game: model.Game, board: chess.Board, +def pgn_game_record(li: lichess.Lichess, config: Configuration, game: model.Game, board: chess.Board, engine: engine_wrapper.EngineWrapper) -> str: """ Return the text of the game's PGN. @@ -1052,11 +1051,10 @@ def create_valid_path(s: str) -> str: if config.pgn_file_grouping == "game" or not game_is_over or force_single: return create_valid_path(f"{white_name} vs {black_name} - {game_id}.pgn") - elif config.pgn_file_grouping == "opponent": + if config.pgn_file_grouping == "opponent": opponent_name = white_name if user_name == black_name else black_name return create_valid_path(f"{user_name} games vs. {opponent_name}.pgn") - else: # config.pgn_file_grouping == "all" - return create_valid_path(f"{user_name} games.pgn") + return create_valid_path(f"{user_name} games.pgn") # config.pgn_file_grouping == "all" def fill_missing_pgn_headers(game_record: chess.pgn.Game, game: model.Game) -> None: diff --git a/lib/matchmaking.py b/lib/matchmaking.py index 39fffafe3..490779514 100644 --- a/lib/matchmaking.py +++ b/lib/matchmaking.py @@ -3,18 +3,16 @@ import logging import datetime import contextlib -import test_bot.lichess from lib import model from lib.timer import Timer, seconds, minutes, days, years from collections import defaultdict from collections.abc import Sequence -from lib import lichess +from lib.lichess import Lichess from lib.config import Configuration from typing import Optional, Union from lib.lichess_types import UserProfileType, PerfType, EventType, FilterType MULTIPROCESSING_LIST_TYPE = Sequence[model.Challenge] DAILY_TIMERS_TYPE = list[Timer] -LICHESS_TYPE = Union[lichess.Lichess, test_bot.lichess.Lichess] logger = logging.getLogger(__name__) @@ -45,7 +43,7 @@ def write_daily_challenges(daily_challenges: DAILY_TIMERS_TYPE) -> None: class Matchmaking: """Challenge other bots.""" - def __init__(self, li: LICHESS_TYPE, config: Configuration, user_profile: UserProfileType) -> None: + def __init__(self, li: Lichess, config: Configuration, user_profile: UserProfileType) -> None: """Initialize values needed for matchmaking.""" self.li = li self.variants = list(filter(lambda variant: variant != "fromPosition", config.challenge.variants)) @@ -378,13 +376,12 @@ def game_category(variant: str, base_time: int, increment: int, days: int) -> st game_duration = base_time + increment * 40 if variant != "standard": return variant - elif days: + if days: return "correspondence" - elif game_duration < 179: + if game_duration < 179: return "bullet" - elif game_duration < 479: + if game_duration < 479: return "blitz" - elif game_duration < 1499: + if game_duration < 1499: return "rapid" - else: - return "classical" + return "classical" diff --git a/lib/model.py b/lib/model.py index 525294acf..6f0922cdb 100644 --- a/lib/model.py +++ b/lib/model.py @@ -59,12 +59,11 @@ def is_supported_time_control(self, challenge_cfg: Configuration) -> bool: # Normal clock game return (increment_min <= self.increment <= increment_max and base_min <= self.base <= base_max) - elif self.days is not None: + if self.days is not None: # Correspondence game return days_min <= self.days <= days_max - else: - # Unlimited game - return days_max == math.inf + # Unlimited game + return days_max == math.inf def is_supported_mode(self, challenge_cfg: Configuration) -> bool: """Check whether the mode is supported.""" @@ -193,8 +192,7 @@ def pgn_event(self) -> str: """Get the event to write in the PGN file.""" if self.variant_name in ["Standard", "From Position"]: return f"{self.mode.title()} {self.perf_name.title()} game" - else: - return f"{self.mode.title()} {self.variant_name} game" + return f"{self.mode.title()} {self.variant_name} game" def time_control(self) -> str: """Get the time control of the game.""" @@ -284,9 +282,8 @@ def __str__(self) -> str: """Get a string representation of `Player`.""" if self.aiLevel: return self.name - else: - rating = f'{self.rating}{"?" if self.provisional else ""}' - return f'{self.title or ""} {self.name} ({rating})'.strip() + rating = f'{self.rating}{"?" if self.provisional else ""}' + return f'{self.title or ""} {self.name} ({rating})'.strip() def __repr__(self) -> str: """Get a string representation of `Player`.""" diff --git a/lib/timer.py b/lib/timer.py index 62f4080f1..a7dc303a7 100644 --- a/lib/timer.py +++ b/lib/timer.py @@ -55,6 +55,9 @@ def years(time_in_years: float) -> timedelta: return days(365) * time_in_years +zero_seconds = seconds(0) + + class Timer: """ A timer for use in lichess-bot. An instance of timer can be used both as a countdown timer and a stopwatch. @@ -69,9 +72,7 @@ class Timer: the timer was created or since it was last reset. """ - __slots__ = ["duration", "starting_time"] - - def __init__(self, duration: timedelta = seconds(0), + def __init__(self, duration: timedelta = zero_seconds, backdated_timestamp: Optional[datetime] = None) -> None: """ Start the timer. diff --git a/test_bot/lichess.py b/test_bot/lichess.py index 6f6f0dd4a..7f65326ce 100644 --- a/test_bot/lichess.py +++ b/test_bot/lichess.py @@ -1,18 +1,18 @@ """Imitate `lichess.py`. Used in tests.""" import time -import chess import chess.engine import json import logging import traceback import datetime from queue import Queue +from requests.models import Response from typing import Union, Optional, Generator +from lib.lichess import Lichess as OriginalLichess from lib.timer import to_msec from lib.lichess_types import (UserProfileType, ChallengeType, REQUESTS_PAYLOAD_TYPE, GameType, OnlineType, PublicDataType, BackoffDetails) -# Unused method argument # ruff: noqa: ARG002 logger = logging.getLogger(__name__) @@ -31,7 +31,7 @@ def is_final(error: Exception) -> bool: return False -class GameStream: +class GameStream(Response): """Imitate lichess.org's GameStream. Used in tests.""" def __init__(self, @@ -47,7 +47,8 @@ def __init__(self, self.board_queue = board_queue self.clock_queue = clock_queue - def iter_lines(self) -> Generator[bytes, None, None]: + def iter_lines(self, chunk_size: Optional[int] = 512, decode_unicode: bool = False, + delimiter: Union[str, bytes, None] = None) -> Generator[bytes, None, None]: """Send the game events to lichess-bot.""" yield json.dumps( {"id": "zzzzzzzz", @@ -103,7 +104,7 @@ def iter_lines(self) -> Generator[bytes, None, None]: yield json.dumps(new_game_state).encode("utf-8") -class EventStream: +class EventStream(Response): """Imitate lichess.org's EventStream. Used in tests.""" def __init__(self, sent_game: bool = False) -> None: @@ -114,7 +115,8 @@ def __init__(self, sent_game: bool = False) -> None: """ self.sent_game = sent_game - def iter_lines(self) -> Generator[bytes, None, None]: + def iter_lines(self, chunk_size: Optional[int] = 512, decode_unicode: bool = False, + delimiter: Union[str, bytes, None] = None) -> Generator[bytes, None, None]: """Send the events to lichess-bot.""" if self.sent_game: yield b"" @@ -129,7 +131,7 @@ def iter_lines(self) -> Generator[bytes, None, None]: # Docs: https://lichess.org/api. -class Lichess: +class Lichess(OriginalLichess): """Imitate communication with lichess.org.""" def __init__(self, @@ -157,8 +159,9 @@ def make_move(self, game_id: str, move: chess.engine.PlayResult) -> None: """Send a move to the opponent engine thread.""" self.move_queue.put(move.move) - def accept_takeback(self, game_id: str, accept: bool) -> None: + def accept_takeback(self, game_id: str, accept: bool) -> bool: """Isn't used in tests.""" + return False def chat(self, game_id: str, room: str, text: str) -> None: """Isn't used in tests.""" diff --git a/test_bot/ruff.toml b/test_bot/ruff.toml index 7a1f45564..feec4646a 100644 --- a/test_bot/ruff.toml +++ b/test_bot/ruff.toml @@ -7,9 +7,7 @@ line-length = 127 select = ["ALL"] ignore = [ - "A004", # Import is shadowing a Python builtin "ANN401", # Dynamically typed expressions (typing.Any) are disallowed - "B008", # Do not perform function call in argument defaults "BLE001", # Do not catch blind exception: Exception "COM812", # Trailing comma missing "D203", # Require blank line after class declaration before docstring @@ -25,16 +23,13 @@ ignore = [ "I001", # Import block is un-sorted or un-formatted "N803", # Argument name should be lowercase "N806", # Variable in function should be lowercase - "N818", # Exception name should be named with an Error suffix "PERF203", # try-except within a loop incurs performance overhead - "PLR0913", #Too many arguments in function definition + "PLR0913", # Too many arguments in function definition "PLR0915", # Too many statements "PLR2004", # Magic value used in comparison, consider replacing `20` with a constant variable "PLW0603", # Using the global statement to update variable is discouraged "PT018", # Assertion should be broken down into multiple parts "PTH", # Replace builtin functions with Path methods - "RET505", # Unnecessary else after return statement - "RET508", # Unnecessary elif after break statement "RUF005", # Consider [*list1, None] instead of concatenation (list1 + [None]) "RUF021", # Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear "S101", # Use of assert detected diff --git a/test_bot/test_external_moves.py b/test_bot/test_external_moves.py index 339d860a1..b9fbc54b8 100644 --- a/test_bot/test_external_moves.py +++ b/test_bot/test_external_moves.py @@ -5,11 +5,10 @@ import os import chess import logging -import test_bot.lichess import chess.engine from datetime import timedelta from copy import deepcopy -from requests.exceptions import ConnectionError, HTTPError, ReadTimeout, RequestException +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 Optional, Union, cast @@ -17,7 +16,6 @@ from lib.config import Configuration, insert_default_values from lib.model import Game from lib.engine_wrapper import get_online_move, get_book_move -LICHESS_TYPE = Union[Lichess, test_bot.lichess.Lichess] class MockLichess(Lichess): @@ -33,7 +31,7 @@ def online_book_get(self, path: str, params: Optional[dict[str, Union[str, int]] """Get an external move from online sources (chessdb or lichess.org).""" @backoff.on_exception(backoff.constant, - (RemoteDisconnected, ConnectionError, HTTPError, ReadTimeout), + (RemoteDisconnected, RequestsConnectionError, HTTPError, ReadTimeout), max_time=60, max_tries=self.max_retries, interval=0.1, @@ -120,7 +118,7 @@ def download_opening_book() -> None: os.makedirs("TEMP", exist_ok=True) -def get_online_move_wrapper(li: LICHESS_TYPE, board: chess.Board, game: Game, online_moves_cfg: Configuration, +def get_online_move_wrapper(li: Lichess, board: chess.Board, game: Game, online_moves_cfg: Configuration, draw_or_resign_cfg: Configuration) -> chess.engine.PlayResult: """Wrap `lib.engine_wrapper.get_online_move` so that it only returns a PlayResult type.""" return cast(chess.engine.PlayResult, get_online_move(li, board, game, online_moves_cfg, draw_or_resign_cfg)) From ef3899cbd3393e68418492b7f01717a26bbc0c4e Mon Sep 17 00:00:00 2001 From: "Ioannis \"Giannis\" Pantidis" <40605232+AttackingOrDefending@users.noreply.github.com> Date: Sat, 1 Feb 2025 13:02:04 +0100 Subject: [PATCH 2/4] Add files via upload --- lib/engine_wrapper.py | 34 +++++++++++++++++++--------------- lib/lichess_bot.py | 5 +++-- lib/model.py | 10 ++++++---- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/engine_wrapper.py b/lib/engine_wrapper.py index aadb8d130..5ac4f4065 100644 --- a/lib/engine_wrapper.py +++ b/lib/engine_wrapper.py @@ -351,7 +351,8 @@ def readable_time(self, number: int) -> str: minutes, seconds = divmod(number, 60) if minutes >= 1: return f"{minutes:0.0f}m {seconds:0.1f}s" - return f"{seconds:0.1f}s" + else: + return f"{seconds:0.1f}s" def readable_number(self, number: int) -> str: """Convert number to a more human-readable format. e.g. 123456789 -> 123M.""" @@ -1065,14 +1066,16 @@ def good_enough(possible_move: LichessEGTBMoveType) -> bool: wdl = best_wdl logger.info(f"Suggesting moves from tablebase.lichess.ovh (wdl: {wdl}) for game {game.id}") return move_list, wdl, {"string": "lichess-bot-source:Lichess EGTB"} - best_move = possible_moves[0] - move = best_move["uci"] - wdl = name_to_wld[best_move["category"]] * -1 - dtz = best_move["dtz"] * -1 - dtm = best_move["dtm"] - if dtm: - dtm *= -1 - logger.info(f"Got move {move} from tablebase.lichess.ovh (wdl: {wdl}, dtz: {dtz}, dtm: {dtm}) for game {game.id}") + else: + best_move = possible_moves[0] + move = best_move["uci"] + wdl = name_to_wld[best_move["category"]] * -1 + dtz = best_move["dtz"] * -1 + dtm = best_move["dtm"] + if dtm: + dtm *= -1 + logger.info(f"Got move {move} from tablebase.lichess.ovh (wdl: {wdl}, dtz: {dtz}, dtm: {dtm})" + f" for game {game.id}") return move, wdl, {"string": "lichess-bot-source:Lichess EGTB"} return None, -3, {} @@ -1119,12 +1122,13 @@ def good_enough(move: ChessDBMoveType) -> bool: move_list = [move["uci"] for move in possible_moves] logger.info(f"Suggesting moves from from chessdb.cn (wdl: {wdl}) for game {game.id}") return move_list, wdl, {"string": "lichess-bot-source:ChessDB EGTB"} - best_move = possible_moves[0] - score = best_move["score"] - move = best_move["uci"] - wdl = score_to_wdl(score) - dtz = score_to_dtz(score) - logger.info(f"Got move {move} from chessdb.cn (wdl: {wdl}, dtz: {dtz}) for game {game.id}") + else: + best_move = possible_moves[0] + score = best_move["score"] + move = best_move["uci"] + wdl = score_to_wdl(score) + dtz = score_to_dtz(score) + logger.info(f"Got move {move} from chessdb.cn (wdl: {wdl}, dtz: {dtz}) for game {game.id}") return move, wdl, {"string": "lichess-bot-source:ChessDB EGTB"} return None, -3, {} diff --git a/lib/lichess_bot.py b/lib/lichess_bot.py index ed628fba6..e638db743 100644 --- a/lib/lichess_bot.py +++ b/lib/lichess_bot.py @@ -1051,10 +1051,11 @@ def create_valid_path(s: str) -> str: if config.pgn_file_grouping == "game" or not game_is_over or force_single: return create_valid_path(f"{white_name} vs {black_name} - {game_id}.pgn") - if config.pgn_file_grouping == "opponent": + elif config.pgn_file_grouping == "opponent": opponent_name = white_name if user_name == black_name else black_name return create_valid_path(f"{user_name} games vs. {opponent_name}.pgn") - return create_valid_path(f"{user_name} games.pgn") # config.pgn_file_grouping == "all" + else: # config.pgn_file_grouping == "all" + return create_valid_path(f"{user_name} games.pgn") def fill_missing_pgn_headers(game_record: chess.pgn.Game, game: model.Game) -> None: diff --git a/lib/model.py b/lib/model.py index 6f0922cdb..df023eaad 100644 --- a/lib/model.py +++ b/lib/model.py @@ -59,11 +59,12 @@ def is_supported_time_control(self, challenge_cfg: Configuration) -> bool: # Normal clock game return (increment_min <= self.increment <= increment_max and base_min <= self.base <= base_max) - if self.days is not None: + elif self.days is not None: # Correspondence game return days_min <= self.days <= days_max - # Unlimited game - return days_max == math.inf + else: + # Unlimited game + return days_max == math.inf def is_supported_mode(self, challenge_cfg: Configuration) -> bool: """Check whether the mode is supported.""" @@ -192,7 +193,8 @@ def pgn_event(self) -> str: """Get the event to write in the PGN file.""" if self.variant_name in ["Standard", "From Position"]: return f"{self.mode.title()} {self.perf_name.title()} game" - return f"{self.mode.title()} {self.variant_name} game" + else: + return f"{self.mode.title()} {self.variant_name} game" def time_control(self) -> str: """Get the time control of the game.""" From 0416c39314401aea60d635cc94f04352c66a0361 Mon Sep 17 00:00:00 2001 From: "Ioannis \"Giannis\" Pantidis" <40605232+AttackingOrDefending@users.noreply.github.com> Date: Sat, 1 Feb 2025 13:02:21 +0100 Subject: [PATCH 3/4] Add files via upload --- test_bot/ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test_bot/ruff.toml b/test_bot/ruff.toml index feec4646a..b1f43e1fd 100644 --- a/test_bot/ruff.toml +++ b/test_bot/ruff.toml @@ -30,6 +30,7 @@ ignore = [ "PLW0603", # Using the global statement to update variable is discouraged "PT018", # Assertion should be broken down into multiple parts "PTH", # Replace builtin functions with Path methods + "RET505", # Unnecessary else after return statement "RUF005", # Consider [*list1, None] instead of concatenation (list1 + [None]) "RUF021", # Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear "S101", # Use of assert detected From 4e87198567981af4c5610492a87aaa500131ebb6 Mon Sep 17 00:00:00 2001 From: "Ioannis \"Giannis\" Pantidis" <40605232+AttackingOrDefending@users.noreply.github.com> Date: Mon, 3 Feb 2025 09:51:00 +0100 Subject: [PATCH 4/4] Add files via upload --- lib/engine_wrapper.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/engine_wrapper.py b/lib/engine_wrapper.py index 5ac4f4065..e43972db4 100644 --- a/lib/engine_wrapper.py +++ b/lib/engine_wrapper.py @@ -185,13 +185,13 @@ def play_move(self, best_move = self.search(board, time_limit, can_ponder, draw_offered, best_move) except chess.engine.EngineError as error: BadMove = (chess.IllegalMoveError, chess.InvalidMoveError) - if any(isinstance(e, BadMove) for e in error.args): - logger.error("Ending game due to bot attempting an illegal move.") - logger.error(error) - game_ender = li.abort if game.is_abortable() else li.resign - game_ender(game.id) - return - raise + if not any(isinstance(e, BadMove) for e in error.args): + raise + logger.error("Ending game due to bot attempting an illegal move.") + logger.error(error) + game_ender = li.abort if game.is_abortable() else li.resign + game_ender(game.id) + return # Heed min_time elapsed = setup_timer.time_since_reset()