From f027531a1b374c19ea8b991e1c4eefb6fe71aa04 Mon Sep 17 00:00:00 2001 From: Trevor Bayless <3620552+trevorbayless@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:29:36 -0500 Subject: [PATCH] Finish GameMetadata rework --- src/cli_chess/core/game/__init__.py | 2 +- src/cli_chess/core/game/game_metadata.py | 49 +++++---- .../core/game/game_presenter_base.py | 3 +- src/cli_chess/core/game/game_view_base.py | 5 +- .../game/offline_game/offline_game_model.py | 2 +- .../game/online_game/online_game_model.py | 101 +++++++++--------- .../game/online_game/online_game_presenter.py | 4 +- .../core/game/online_game/online_game_view.py | 4 +- .../online_game/watch_tv/watch_tv_model.py | 27 ++--- .../online_game/watch_tv/watch_tv_view.py | 4 +- .../modules/clock/clock_presenter.py | 11 +- .../player_info/player_info_presenter.py | 4 +- .../modules/player_info/player_info_view.py | 10 +- 13 files changed, 114 insertions(+), 112 deletions(-) diff --git a/src/cli_chess/core/game/__init__.py b/src/cli_chess/core/game/__init__.py index 1c15521..90cd291 100644 --- a/src/cli_chess/core/game/__init__.py +++ b/src/cli_chess/core/game/__init__.py @@ -3,4 +3,4 @@ from .game_presenter_base import GamePresenterBase, PlayableGamePresenterBase from .online_game.online_game_presenter import start_online_game from .offline_game.offline_game_presenter import start_offline_game -from .game_metadata import GameMetadata, Player +from .game_metadata import GameMetadata, PlayerMetadata, ClockMetadata diff --git a/src/cli_chess/core/game/game_metadata.py b/src/cli_chess/core/game/game_metadata.py index dcc7d6e..7c5ae74 100644 --- a/src/cli_chess/core/game/game_metadata.py +++ b/src/cli_chess/core/game/game_metadata.py @@ -1,36 +1,41 @@ from dataclasses import dataclass +from chess import Color +from typing import Optional @dataclass -class Player: - title: str = None - name: str = None - rating: str = None - ai_level: str = None # only used online - rating_diff: int = None - is_provisional_rating: bool = False # only used online +class PlayerMetadata: + title: Optional[str] = None + name: Optional[str] = None + rating: Optional[str] = None + rating_diff: Optional[int] = None + is_provisional_rating: bool = False + ai_level: Optional[str] = None @dataclass -class Clock: +class ClockMetadata: units: str = "ms" - time: int = 0 - increment: int = 0 + time: Optional[int] = None + increment: Optional[int] = None @dataclass -class GameStatus: - status: str = None - winner: str = None +class GameStatusMetadata: + status: Optional[str] = None + winner: Optional[str] = None -@dataclass class GameMetadata: - players = [Player(), Player()] - clocks = [Clock(), Clock()] - game_status = GameStatus() - game_id: str = None - variant: str = None - my_color: str = None # TODO: Find a better solution - rated: bool = False # only used online - speed: str = None # only used online + def __init__(self): + self.players = [PlayerMetadata(), PlayerMetadata()] + self.clocks = [ClockMetadata(), ClockMetadata()] + self.game_status = GameStatusMetadata() + self.game_id: Optional[str] = None + self.variant: Optional[str] = None + self.my_color: Optional[Color] = None + self.rated: bool = False + self.speed: Optional[str] = None + + def reset(self): + self.__init__() diff --git a/src/cli_chess/core/game/game_presenter_base.py b/src/cli_chess/core/game/game_presenter_base.py index 817fc6e..89edcf2 100644 --- a/src/cli_chess/core/game/game_presenter_base.py +++ b/src/cli_chess/core/game/game_presenter_base.py @@ -20,8 +20,7 @@ def __init__(self, model: GameModelBase): self.move_list_presenter = MoveListPresenter(model.move_list_model) self.material_diff_presenter = MaterialDifferencePresenter(model.material_diff_model) self.player_info_presenter = PlayerInfoPresenter(model) - # TODO: Revert this after testing - # self.clock_presenter = ClockPresenter(model) + self.clock_presenter = ClockPresenter(model) self.view = self._get_view() self.model.e_game_model_updated.add_listener(self.update) diff --git a/src/cli_chess/core/game/game_view_base.py b/src/cli_chess/core/game/game_view_base.py index fa93012..ba064f7 100644 --- a/src/cli_chess/core/game/game_view_base.py +++ b/src/cli_chess/core/game/game_view_base.py @@ -23,9 +23,8 @@ def __init__(self, presenter: GamePresenterBase) -> None: self.material_diff_lower_container = presenter.material_diff_presenter.view_lower self.player_info_upper_container = presenter.player_info_presenter.view_upper self.player_info_lower_container = presenter.player_info_presenter.view_lower - # TODO: Revert this after testing - # self.clock_upper = presenter.clock_presenter.view_upper - # self.clock_lower = presenter.clock_presenter.view_lower + self.clock_upper = presenter.clock_presenter.view_upper + self.clock_lower = presenter.clock_presenter.view_lower self.alert = AlertContainer() self._container = self._create_container() diff --git a/src/cli_chess/core/game/offline_game/offline_game_model.py b/src/cli_chess/core/game/offline_game/offline_game_model.py index 84c7a4d..684b38e 100644 --- a/src/cli_chess/core/game/offline_game/offline_game_model.py +++ b/src/cli_chess/core/game/offline_game/offline_game_model.py @@ -79,7 +79,7 @@ def _update_game_metadata(self, **kwargs) -> None: try: if 'game_parameters' in kwargs: data = kwargs['game_parameters'] - self.game_metadata.my_color = COLOR_NAMES[self.my_color] + self.game_metadata.my_color = self.my_color self.game_metadata.variant = data[GameOption.VARIANT] self.game_metadata.players[self.my_color].name = player_info_config.get_value(player_info_config.Keys.OFFLINE_PLAYER_NAME) # noqa: E501 diff --git a/src/cli_chess/core/game/online_game/online_game_model.py b/src/cli_chess/core/game/online_game/online_game_model.py index e996936..0244e16 100644 --- a/src/cli_chess/core/game/online_game/online_game_model.py +++ b/src/cli_chess/core/game/online_game/online_game_model.py @@ -2,7 +2,7 @@ from cli_chess.core.game.game_options import GameOption from cli_chess.core.api import GameStateDispatcher from cli_chess.utils import log, threaded, RequestSuccessfullySent -from chess import COLOR_NAMES, WHITE +from chess import COLORS, COLOR_NAMES, WHITE, BLACK, Color from typing import Optional @@ -12,12 +12,11 @@ class OnlineGameModel(PlayableGameModelBase): """ def __init__(self, game_parameters: dict, is_vs_ai: bool): super().__init__(play_as_color=game_parameters[GameOption.COLOR], variant=game_parameters[GameOption.VARIANT], fen=None) - self._update_game_metadata(game_parameters=game_parameters) - - self.game_state_dispatcher = Optional[GameStateDispatcher] + self.vs_ai = is_vs_ai self.playing_game_id = None self.searching = False - self.vs_ai = is_vs_ai + self._update_game_metadata(game_parameters=game_parameters) + self.game_state_dispatcher = Optional[GameStateDispatcher] try: from cli_chess.core.api.api_manager import api_client, api_iem @@ -37,17 +36,17 @@ def create_game(self) -> None: self.searching = True if self.vs_ai: # Challenge Lichess AI (stockfish) - self.api_client.challenges.create_ai(level=self.game_metadata['ai_level'], - clock_limit=self.game_metadata['clock']['white']['time'], - clock_increment=self.game_metadata['clock']['white']['increment'], - color=self.game_metadata['my_color_str'], - variant=self.game_metadata['variant']) + self.api_client.challenges.create_ai(level=self.game_metadata.players[not self.my_color].ai_level, + clock_limit=self.game_metadata.clocks[WHITE].time * 60, # challenges need time in seconds + clock_increment=self.game_metadata.clocks[WHITE].increment, + color=COLOR_NAMES[self.game_metadata.my_color], + variant=self.game_metadata.variant) else: # Find a random opponent - self.api_client.board.seek(time=self.game_metadata['clock']['white']['time'], # Both players initially have the same time - increment=self.game_metadata['clock']['white']['increment'], - color=self.game_metadata['my_color_str'], - variant=self.game_metadata['variant'], - rated=self.game_metadata['rated'], + self.api_client.board.seek(time=self.game_metadata.clocks[WHITE].time, + increment=self.game_metadata.clocks[WHITE].increment, + color=COLOR_NAMES[self.game_metadata.my_color], + variant=self.game_metadata.variant, + rated=self.game_metadata.rated, rating_range=None) def _start_game(self, game_id: str) -> None: @@ -92,7 +91,7 @@ def handle_game_state_dispatcher_event(self, **kwargs) -> None: if 'gameFull' in kwargs: event = kwargs['gameFull'] self._update_game_metadata(gsd_gameFull=event) - self.board_model.reinitialize_board(variant=self.game_metadata['variant'], + self.board_model.reinitialize_board(variant=self.game_metadata.variant, orientation=(self.my_color if self.board_model.get_variant_name() != "racingkings" else WHITE), fen=event.get('initialFen', "")) self.board_model.make_moves_from_list(event.get('state', {}).get('moves', []).split()) @@ -219,56 +218,52 @@ def _update_game_metadata(self, **kwargs) -> None: try: if 'game_parameters' in kwargs: # This is the data that came from the menu selections data = kwargs['game_parameters'] - self.game_metadata['my_color_str'] = COLOR_NAMES[self.my_color] - self.game_metadata['variant'] = data.get(GameOption.VARIANT) - self.game_metadata['rated'] = data.get(GameOption.RATED, False) # Games against AI will not have this data - self.game_metadata['ai_level'] = data.get(GameOption.COMPUTER_SKILL_LEVEL) # Only games against AI will have this data - self.game_metadata['clock']['white']['time'] = data.get(GameOption.TIME_CONTROL)[0] # mins - self.game_metadata['clock']['white']['increment'] = data.get(GameOption.TIME_CONTROL)[1] # secs - self.game_metadata['clock']['black'] = self.game_metadata['clock']['white'] - - if self.game_metadata['ai_level']: - self.game_metadata['clock']['white']['time'] = data.get(GameOption.TIME_CONTROL)[0] * 60 # challenges need time in seconds - self.game_metadata['clock']['black'] = self.game_metadata['clock']['white'] + self.game_metadata.my_color = self.my_color + self.game_metadata.variant = data.get(GameOption.VARIANT) + self.game_metadata.rated = data.get(GameOption.RATED, False) + self.game_metadata.players[not self.my_color].ai_level = data.get(GameOption.COMPUTER_SKILL_LEVEL) if self.vs_ai else None + + for color in COLORS: + self.game_metadata.clocks[color].time = data.get(GameOption.TIME_CONTROL)[0] # mins + self.game_metadata.clocks[color].increment = data.get(GameOption.TIME_CONTROL)[1] # secs elif 'iem_gameStart' in kwargs: - # Reset game metadata - # self.game_metadata = self._default_game_metadata() + self.game_metadata.reset() data = kwargs['iem_gameStart'] - self.game_metadata['gameId'] = data.get('gameId') - self.game_metadata['my_color_str'] = data.get('color') - self.game_metadata['rated'] = data.get('rated') - self.game_metadata['variant'] = data.get('variant', {}).get('name') - self.game_metadata['speed'] = data['speed'] + self.game_metadata.game_id = data.get('gameId') + self.game_metadata.my_color = self.my_color + self.game_metadata.rated = data.get('rated') + self.game_metadata.variant = data.get('variant', {}).get('name') + self.game_metadata.speed = data['speed'] elif 'iem_gameFinish' in kwargs: data = kwargs['iem_gameFinish'] - self.game_metadata['players'][COLOR_NAMES[self.my_color]]['rating_diff'] = data.get('ratingDiff', "") - self.game_metadata['players'][COLOR_NAMES[not self.my_color]]['rating_diff'] = data.get('opponent', {}).get('ratingDiff', "") + self.game_metadata.players[self.my_color].rating_diff = data.get('ratingDiff', "") + self.game_metadata.players[not self.my_color].rating_diff = data.get('opponent', {}).get('ratingDiff', "") elif 'gsd_gameFull' in kwargs: data = kwargs['gsd_gameFull'] for color in COLOR_NAMES: - if data.get(color, {}).get('name'): - self.game_metadata['players'][color]['title'] = data.get(color, {}).get('title') - self.game_metadata['players'][color]['name'] = data.get(color, {}).get('name', "?") - self.game_metadata['players'][color]['rating'] = data.get(color, {}).get('rating', "?") - self.game_metadata['players'][color]['provisional'] = data.get(color, {}).get('provisional', False) - elif data.get(color, {}).get('aiLevel'): - self.game_metadata['players'][color]['name'] = f"Stockfish level {data.get(color, {}).get('aiLevel', '?')}" - - self.game_metadata['clock']['units'] = "ms" - self.game_metadata['clock']['white']['time'] = data.get('state', {}).get('wtime') - self.game_metadata['clock']['white']['increment'] = data.get('state', {}).get('winc') - self.game_metadata['clock']['black']['time'] = data.get('state', {}).get('btime') - self.game_metadata['clock']['black']['increment'] = data.get('state', {}).get('binc') + side_data = data.get(color, {}) + color_as_bool = Color(COLOR_NAMES.index(color)) + if side_data.get('name'): + self.game_metadata.players[color_as_bool].title = side_data.get('title') + self.game_metadata.players[color_as_bool].name = side_data.get('name', "?") + self.game_metadata.players[color_as_bool].rating = side_data.get('rating', "?") + self.game_metadata.players[color_as_bool].is_provisional_rating = side_data.get('provisional', False) + elif self.vs_ai: + self.game_metadata.players[color_as_bool].name = f"Stockfish level {side_data.get('aiLevel', '?')}" + + self.game_metadata.clocks[color_as_bool].units = "ms" + self.game_metadata.clocks[color_as_bool].time = data.get('state', {}).get('wtime' if color == "white" else 'btime') + self.game_metadata.clocks[color_as_bool].increment = data.get('state', {}).get('winc' if color == "white" else 'binc') elif 'gsd_gameState' in kwargs: data = kwargs['gsd_gameState'] - self.game_metadata['clock']['white']['time'] = data.get('wtime') - self.game_metadata['clock']['black']['time'] = data.get('btime') + self.game_metadata.clocks[WHITE].time = data.get('wtime') + self.game_metadata.clocks[BLACK].time = data.get('btime') self._notify_game_model_updated() except Exception as e: @@ -280,8 +275,8 @@ def _report_game_over(self, status: str, winner: str) -> None: This should only ever be called if the game is confirmed to be over """ self._game_end() - self.game_metadata['state']['status'] = status # status list can be found in lila status.ts - self.game_metadata['state']['winner'] = winner + self.game_metadata.game_status.status = status # status list can be found in lila status.ts + self.game_metadata.game_status.winner = winner self._notify_game_model_updated(onlineGameOver=True) def cleanup(self) -> None: diff --git a/src/cli_chess/core/game/online_game/online_game_presenter.py b/src/cli_chess/core/game/online_game/online_game_presenter.py index 5a4906f..c442095 100644 --- a/src/cli_chess/core/game/online_game/online_game_presenter.py +++ b/src/cli_chess/core/game/online_game/online_game_presenter.py @@ -38,8 +38,8 @@ def update(self, **kwargs) -> None: def _parse_and_present_game_over(self) -> None: """Triages game over status for parsing and sending to the view for display""" if not self.is_game_in_progress(): - status: str = self.model.game_metadata['state']['status'] - winner_str = self.model.game_metadata['state']['winner'] + status = self.model.game_metadata.game_status.status + winner_str = self.model.game_metadata.game_status.winner if winner_str: # Handle win/loss output self._display_win_loss_output(status, winner_str) diff --git a/src/cli_chess/core/game/online_game/online_game_view.py b/src/cli_chess/core/game/online_game/online_game_view.py index 8e82c80..ce88059 100644 --- a/src/cli_chess/core/game/online_game/online_game_view.py +++ b/src/cli_chess/core/game/online_game/online_game_view.py @@ -18,13 +18,13 @@ def _create_container(self) -> Container: VSplit([ self.board_output_container, HSplit([ - #self.clock_upper, + self.clock_upper, self.player_info_upper_container, self.material_diff_upper_container, self.move_list_container, self.material_diff_lower_container, self.player_info_lower_container, - #self.clock_lower + self.clock_lower ]) ]), self.input_field_container, diff --git a/src/cli_chess/core/game/online_game/watch_tv/watch_tv_model.py b/src/cli_chess/core/game/online_game/watch_tv/watch_tv_model.py index d12859e..1b6cb98 100644 --- a/src/cli_chess/core/game/online_game/watch_tv/watch_tv_model.py +++ b/src/cli_chess/core/game/online_game/watch_tv/watch_tv_model.py @@ -1,8 +1,8 @@ -from cli_chess.core.game import GameModelBase, GameMetadata +from cli_chess.core.game import GameModelBase from cli_chess.menus.tv_channel_menu import TVChannelMenuOptions from cli_chess.utils.event import Event from cli_chess.utils.logging import log -from chess import COLOR_NAMES, Color, WHITE, BLACK +from chess import COLOR_NAMES, COLORS, Color, WHITE, BLACK from berserk.exceptions import ResponseError from time import sleep import threading @@ -30,7 +30,7 @@ def _update_game_metadata(self, **kwargs) -> None: if 'tv_descriptionEvent' in kwargs: data = kwargs['tv_descriptionEvent'] if 'tv_startGameEvent' in kwargs: - self.game_metadata = GameMetadata() + self.game_metadata.reset() self.game_metadata.game_id = data.get('id') self.game_metadata.rated = data.get('rated') @@ -43,20 +43,21 @@ def _update_game_metadata(self, **kwargs) -> None: color_as_bool = Color(COLOR_NAMES.index(color)) side_data = data.get('players', {}).get(color, {}) player_data = side_data.get('user') - ai_level = player_data.get('aiLevel') - if side_data and player_data: # non-ai player data - self.game_metadata.players[color_as_bool].name = player_data.get('name', "") - self.game_metadata.players[color_as_bool].rating = side_data.get('rating', "") + ai_level = side_data.get('aiLevel') + if side_data and player_data: + self.game_metadata.players[color_as_bool].title = player_data.get('title') + self.game_metadata.players[color_as_bool].name = player_data.get('name', "?") + self.game_metadata.players[color_as_bool].rating = side_data.get('rating', "?") + self.game_metadata.players[color_as_bool].is_provisional_rating = side_data.get('provisional', False) self.game_metadata.players[color_as_bool].rating_diff = side_data.get('ratingDiff', "") - elif ai_level: # ai data + elif ai_level: self.game_metadata.players[color_as_bool].name = f"Stockfish level {ai_level}" if 'tv_coreGameEvent' in kwargs: data = kwargs['tv_coreGameEvent'] - self.game_metadata.clocks[WHITE].units = "sec" - self.game_metadata.clocks[BLACK].units = "sec" - self.game_metadata.clocks[WHITE].time = data.get('wc') - self.game_metadata.clocks[BLACK].time = data.get('bc') + for color in COLORS: + self.game_metadata.clocks[color].units = "sec" + self.game_metadata.clocks[color].time = data.get('wc' if color == WHITE else 'bc') self.e_game_model_updated.notify() except Exception as e: @@ -77,7 +78,7 @@ def stream_event_received(self, **kwargs): variant = event.get('variant', {}).get('key') white_rating = int(event.get('players', {}).get('white', {}).get('rating') or 0) black_rating = int(event.get('players', {}).get('black', {}).get('rating') or 0) - orientation = True if ((white_rating >= black_rating) or self.channel.key == "racingKings") else False + orientation = WHITE if ((white_rating >= black_rating) or self.channel.key == "racingKings") else BLACK self._update_game_metadata(tv_descriptionEvent=event, tv_startGameEvent=True) last_move = event.get('lastMove', "") diff --git a/src/cli_chess/core/game/online_game/watch_tv/watch_tv_view.py b/src/cli_chess/core/game/online_game/watch_tv/watch_tv_view.py index ba4c306..b1b8c7a 100644 --- a/src/cli_chess/core/game/online_game/watch_tv/watch_tv_view.py +++ b/src/cli_chess/core/game/online_game/watch_tv/watch_tv_view.py @@ -20,13 +20,13 @@ def _create_container(self) -> Container: VSplit([ self.board_output_container, HSplit([ - #self.clock_upper, + self.clock_upper, self.player_info_upper_container, self.material_diff_upper_container, Box(self.move_list_placeholder, height=D(min=1, max=4)), self.material_diff_lower_container, self.player_info_lower_container, - #self.clock_lower + self.clock_lower ]), ]), self.alert diff --git a/src/cli_chess/modules/clock/clock_presenter.py b/src/cli_chess/modules/clock/clock_presenter.py index 0765b45..d7f0412 100644 --- a/src/cli_chess/modules/clock/clock_presenter.py +++ b/src/cli_chess/modules/clock/clock_presenter.py @@ -1,6 +1,6 @@ from __future__ import annotations from cli_chess.modules.clock import ClockView -from chess import Color, COLOR_NAMES +from chess import Color from datetime import datetime, timezone from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -26,17 +26,16 @@ def update(self, **kwargs) -> None: def get_clock_display(self, color: Color) -> str: """Returns the formatted clock display for the color passed in""" - clock_data = self.model.game_metadata.get('clock') - units = clock_data.get('units') - time = clock_data.get(COLOR_NAMES[color]).get('time') + clock_data = self.model.game_metadata.clocks[color] + time = clock_data.time if not time: return "--:--" if not isinstance(time, datetime): - if units == "ms": + if clock_data.units == "ms": time = datetime.fromtimestamp(time / 1000, timezone.utc) - elif units == "sec": + elif clock_data.units == "sec": time = datetime.fromtimestamp(time, timezone.utc) return time.strftime("%M:%S") if not time.hour else time.strftime("%H:%M:%S") diff --git a/src/cli_chess/modules/player_info/player_info_presenter.py b/src/cli_chess/modules/player_info/player_info_presenter.py index 55fac22..ef11549 100644 --- a/src/cli_chess/modules/player_info/player_info_presenter.py +++ b/src/cli_chess/modules/player_info/player_info_presenter.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from cli_chess.core.game import GameModelBase - from cli_chess.core.game import Player + from cli_chess.core.game import PlayerMetadata class PlayerInfoPresenter: @@ -24,6 +24,6 @@ def update(self, **kwargs) -> None: self.view_upper.update(self.get_player_info(not orientation)) self.view_lower.update(self.get_player_info(orientation)) - def get_player_info(self, color: Color) -> Player: + def get_player_info(self, color: Color) -> PlayerMetadata: """Returns the player metadata for the passed in color""" return self.model.game_metadata.players[color] diff --git a/src/cli_chess/modules/player_info/player_info_view.py b/src/cli_chess/modules/player_info/player_info_view.py index 00d8a30..618e3dd 100644 --- a/src/cli_chess/modules/player_info/player_info_view.py +++ b/src/cli_chess/modules/player_info/player_info_view.py @@ -5,12 +5,16 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from cli_chess.modules.player_info import PlayerInfoPresenter - from cli_chess.core.game import Player + from cli_chess.core.game import PlayerMetadata class PlayerInfoView: - def __init__(self, presenter: PlayerInfoPresenter, player_info: Player): + def __init__(self, presenter: PlayerInfoPresenter, player_info: PlayerMetadata): self.presenter = presenter + self.player_title = "" + self.player_name = "" + self.player_rating = "" + self.rating_diff = "" self.update(player_info) self._player_title_control = FormattedTextControl(text=lambda: self.player_title, style="class:player-info.title") @@ -31,7 +35,7 @@ def _create_container(self) -> Container: ], width=D(min=1), height=D(max=1), window_too_small=ConditionalContainer(Window(), False)) - def update(self, player_info: Player) -> None: + def update(self, player_info: PlayerMetadata) -> None: """Updates the player info using the data passed in""" self._set_player_title(player_info.title) self._set_player_name(player_info.name)