From d7be2867fba30d26dd2d4289136966a035d49d46 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Tue, 21 Feb 2023 13:06:30 -0600
Subject: [PATCH 01/25] Add flake8 config
---
setup.cfg | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/setup.cfg b/setup.cfg
index 4ff9b06..2020d46 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -49,3 +49,8 @@ where = src
[options.entry_points]
console_scripts =
cli-chess = cli_chess.__main__:run
+
+[flake8]
+max-line-length = 150
+per-file-ignores =
+ */__init__.py: F401
From 671709089826c3c527892ee4729ae340558e2051 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Tue, 21 Feb 2023 13:09:43 -0600
Subject: [PATCH 02/25] Cleanup
---
src/cli_chess/core/api/game_state_dispatcher.py | 6 +++---
.../core/game/offline_game/offline_game_model.py | 2 +-
.../game/offline_game/offline_game_presenter.py | 2 +-
.../core/game/online_game/online_game_view.py | 6 ------
.../game/online_game/watch_tv/watch_tv_model.py | 2 +-
src/cli_chess/core/main/main_view.py | 2 +-
src/cli_chess/menus/main_menu/main_menu_view.py | 2 +-
src/cli_chess/menus/menu_view.py | 2 +-
.../offline_games_menu_view.py | 2 +-
.../online_games_menu/online_games_menu_model.py | 3 ++-
.../online_games_menu/online_games_menu_view.py | 2 +-
.../menus/settings_menu/settings_menu_model.py | 2 +-
.../vs_computer_menu/vs_computer_menu_model.py | 16 ++++++++--------
src/cli_chess/modules/board/board_model.py | 2 +-
src/cli_chess/modules/engine/engine_model.py | 2 +-
.../modules/token_manager/token_manager_view.py | 2 +-
.../tests/modules/board/test_board_model.py | 1 -
src/cli_chess/tests/modules/test_common.py | 2 +-
src/cli_chess/utils/config.py | 2 +-
19 files changed, 27 insertions(+), 33 deletions(-)
diff --git a/src/cli_chess/core/api/game_state_dispatcher.py b/src/cli_chess/core/api/game_state_dispatcher.py
index 3185b10..3c9ea38 100644
--- a/src/cli_chess/core/api/game_state_dispatcher.py
+++ b/src/cli_chess/core/api/game_state_dispatcher.py
@@ -76,7 +76,7 @@ def make_move(self, move: str):
def send_takeback_request(self) -> None:
"""Sends a takeback request to our opponent"""
try:
- log.debug(f"GameStateDispatcher: Sending takeback offer to opponent")
+ log.debug("GameStateDispatcher: Sending takeback offer to opponent")
self.api_client.board.offer_takeback(self.game_id)
except Exception:
raise
@@ -84,7 +84,7 @@ def send_takeback_request(self) -> None:
def send_draw_offer(self) -> None:
"""Sends a draw offer to our opponent"""
try:
- log.debug(f"GameStateDispatcher: Sending draw offer to opponent")
+ log.debug("GameStateDispatcher: Sending draw offer to opponent")
self.api_client.board.offer_draw(self.game_id)
except Exception:
raise
@@ -92,7 +92,7 @@ def send_draw_offer(self) -> None:
def resign(self) -> None:
"""Resigns the game"""
try:
- log.debug(f"GameStateDispatcher: Sending resignation")
+ log.debug("GameStateDispatcher: Sending resignation")
self.api_client.board.resign_game(self.game_id)
except Exception:
raise
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 be004e8..306262b 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
@@ -95,7 +95,7 @@ def _save_game_metadata(self, **kwargs) -> None:
# Engine information
engine_name = "Fairy-Stockfish" # TODO: Implement a better solution for when other engines are supported
- engine_name = engine_name + f" Lvl {data.get(GameOption.COMPUTER_SKILL_LEVEL)}" if not data.get(GameOption.SPECIFY_ELO) else engine_name
+ engine_name = engine_name + f" Lvl {data.get(GameOption.COMPUTER_SKILL_LEVEL)}" if not data.get(GameOption.SPECIFY_ELO) else engine_name # noqa: E501
self.game_metadata['players'][COLOR_NAMES[not self.my_color]]['title'] = ""
self.game_metadata['players'][COLOR_NAMES[not self.my_color]]['name'] = engine_name
self.game_metadata['players'][COLOR_NAMES[not self.my_color]]['rating'] = data.get(GameOption.COMPUTER_ELO, "")
diff --git a/src/cli_chess/core/game/offline_game/offline_game_presenter.py b/src/cli_chess/core/game/offline_game/offline_game_presenter.py
index fd95376..4017dac 100644
--- a/src/cli_chess/core/game/offline_game/offline_game_presenter.py
+++ b/src/cli_chess/core/game/offline_game/offline_game_presenter.py
@@ -65,4 +65,4 @@ async def make_engine_move(self) -> None:
self.board_presenter.make_move(engine_move.move.uci(), human=False)
except Exception as e:
log.critical(f"Received an invalid move from the engine: {e}")
- self.view.show_error(f"Invalid move received from engine - inspect logs")
+ self.view.show_error("Invalid move received from engine - inspect logs")
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 1483178..4614fd3 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
@@ -15,12 +15,6 @@
from __future__ import annotations
from cli_chess.core.game import PlayableGameViewBase
-from cli_chess.utils.ui_common import handle_mouse_click
-from prompt_toolkit.layout import Window, FormattedTextControl, VSplit, D
-from prompt_toolkit.formatted_text import StyleAndTextTuples
-from prompt_toolkit.key_binding import KeyBindings
-from prompt_toolkit.keys import Keys
-from prompt_toolkit.filters import Condition
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cli_chess.core.game.online_game import OnlineGamePresenter
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 c0dd320..6a1eaa9 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
@@ -84,7 +84,7 @@ def _save_game_metadata(self, **kwargs) -> None:
if data['players'][color].get('user'):
self.game_metadata['players'][color] = data['players'][color]['user']
self.game_metadata['players'][color]['rating'] = data['players'][color]['rating']
- self.game_metadata['players'][color]['ratingDiff'] = data.get('players', {}).get(color, {}).get('ratingDiff', "") # NOTE: Not included on aborted games
+ self.game_metadata['players'][color]['ratingDiff'] = data.get('players', {}).get(color, {}).get('ratingDiff', "") # NOTE: Not included on aborted games # noqa
elif data['players'][color].get('aiLevel'):
self.game_metadata['players'][color]['name'] = f"Stockfish level {data['players'][color]['aiLevel']}"
diff --git a/src/cli_chess/core/main/main_view.py b/src/cli_chess/core/main/main_view.py
index 1268bc2..12cbb37 100644
--- a/src/cli_chess/core/main/main_view.py
+++ b/src/cli_chess/core/main/main_view.py
@@ -74,7 +74,7 @@ def _get_function_bar_fragments() -> StyleAndTextTuples:
Window(FormattedTextControl(f"cli-chess {__version__}"), align=WindowAlign.RIGHT)
], height=D(max=1, preferred=1))
- def _create_function_bar_key_bindings(self) -> "_MergedKeyBindings":
+ def _create_function_bar_key_bindings(self) -> "_MergedKeyBindings": # noqa: F821
"""Creates the key bindings for the function bar"""
main_menu_key_bindings = self.presenter.main_menu_presenter.view.get_function_bar_key_bindings()
diff --git a/src/cli_chess/menus/main_menu/main_menu_view.py b/src/cli_chess/menus/main_menu/main_menu_view.py
index 625d987..44cf507 100644
--- a/src/cli_chess/menus/main_menu/main_menu_view.py
+++ b/src/cli_chess/menus/main_menu/main_menu_view.py
@@ -100,7 +100,7 @@ def get_function_bar_fragments(self) -> StyleAndTextTuples:
fragments = self.presenter.settings_menu_presenter.view.get_function_bar_fragments()
return fragments
- def get_function_bar_key_bindings(self) -> "_MergedKeyBindings":
+ def get_function_bar_key_bindings(self) -> "_MergedKeyBindings": # noqa: F821
"""Returns the appropriate function bar key bindings based on menu item selection"""
online_games_kb = ConditionalKeyBindings(
self.presenter.online_games_menu_presenter.view.get_function_bar_key_bindings(),
diff --git a/src/cli_chess/menus/menu_view.py b/src/cli_chess/menus/menu_view.py
index c039fee..27af327 100644
--- a/src/cli_chess/menus/menu_view.py
+++ b/src/cli_chess/menus/menu_view.py
@@ -152,7 +152,7 @@ def value_click():
sel_class = ",focused-selected"
tokens.append(("class:menu.option" + sel_class, f"{menu_option.option_name:<{self.column_width}}", label_click))
- tokens.append(("class:menu.multi-value" + sel_class, f"{menu_option.values[menu_option.selected_value['index']]:<{self.column_width}}", value_click))
+ tokens.append(("class:menu.multi-value" + sel_class, f"{menu_option.values[menu_option.selected_value['index']]:<{self.column_width}}", value_click)) # noqa: E501
tokens.append(("class:menu", "\n"))
for i, opt in enumerate(self.presenter.get_visible_menu_options()):
diff --git a/src/cli_chess/menus/offline_games_menu/offline_games_menu_view.py b/src/cli_chess/menus/offline_games_menu/offline_games_menu_view.py
index 52819f2..1a245b3 100644
--- a/src/cli_chess/menus/offline_games_menu/offline_games_menu_view.py
+++ b/src/cli_chess/menus/offline_games_menu/offline_games_menu_view.py
@@ -16,7 +16,7 @@
from __future__ import annotations
from cli_chess.menus import MenuView
from cli_chess.menus.offline_games_menu import OfflineGamesMenuOptions
-from prompt_toolkit.layout import Container, Window, FormattedTextControl, ConditionalContainer, VSplit, HSplit
+from prompt_toolkit.layout import Container, ConditionalContainer, VSplit, HSplit
from prompt_toolkit.filters import Condition, is_done
from prompt_toolkit.widgets import Box
from prompt_toolkit.formatted_text import StyleAndTextTuples
diff --git a/src/cli_chess/menus/online_games_menu/online_games_menu_model.py b/src/cli_chess/menus/online_games_menu/online_games_menu_model.py
index 302d554..42248b4 100644
--- a/src/cli_chess/menus/online_games_menu/online_games_menu_model.py
+++ b/src/cli_chess/menus/online_games_menu/online_games_menu_model.py
@@ -28,7 +28,8 @@ def __init__(self):
self.menu = self._create_menu()
super().__init__(self.menu)
- def _create_menu(self) -> MenuCategory:
+ @staticmethod
+ def _create_menu() -> MenuCategory:
"""Create the menu options"""
menu_options = [
MenuOption(OnlineGamesMenuOptions.CREATE_GAME, "Create an online game against a random opponent"),
diff --git a/src/cli_chess/menus/online_games_menu/online_games_menu_view.py b/src/cli_chess/menus/online_games_menu/online_games_menu_view.py
index 4891ce2..e0f1bbf 100644
--- a/src/cli_chess/menus/online_games_menu/online_games_menu_view.py
+++ b/src/cli_chess/menus/online_games_menu/online_games_menu_view.py
@@ -59,7 +59,7 @@ def get_function_bar_fragments(self) -> StyleAndTextTuples:
fragments = self.presenter.tv_channel_menu_presenter.view.get_function_bar_fragments()
return fragments
- def get_function_bar_key_bindings(self) -> "_MergedKeyBindings":
+ def get_function_bar_key_bindings(self) -> "_MergedKeyBindings": # noqa: F821
"""Returns the appropriate function bar key bindings based on menu item selection"""
vs_ai_kb = ConditionalKeyBindings(
self.presenter.vs_computer_menu_presenter.view.get_function_bar_key_bindings(),
diff --git a/src/cli_chess/menus/settings_menu/settings_menu_model.py b/src/cli_chess/menus/settings_menu/settings_menu_model.py
index 0bb9087..4cf1d9a 100644
--- a/src/cli_chess/menus/settings_menu/settings_menu_model.py
+++ b/src/cli_chess/menus/settings_menu/settings_menu_model.py
@@ -32,7 +32,7 @@ def __init__(self):
def _create_menu() -> MenuCategory:
"""Create the menu options"""
menu_options = [
- MenuOption(SettingsMenuOptions.LICHESS_AUTHENTICATION, "Authenticate with Lichess by adding your API access token (required for playing online)"),
+ MenuOption(SettingsMenuOptions.LICHESS_AUTHENTICATION, "Authenticate with Lichess by adding your API access token (required for playing online)"), # noqa: E501
MenuOption(SettingsMenuOptions.GAME_SETTINGS, "Customize the look and feel when playing games"),
MenuOption(SettingsMenuOptions.PROGRAM_SETTINGS, "Customize the look and feel of cli-chess"),
]
diff --git a/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py b/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py
index 3f97165..ceef6e4 100644
--- a/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py
+++ b/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py
@@ -32,10 +32,10 @@ def __init__(self):
def _create_menu() -> MenuCategory:
"""Create the online menu options"""
menu_options = [
- MultiValueMenuOption(GameOption.VARIANT, "Choose the variant to play", [option for option in OnlineGameOptions.variant_options_dict]),
- MultiValueMenuOption(GameOption.TIME_CONTROL, "Choose the time control", [option for option in OnlineGameOptions.time_control_options_dict]),
- MultiValueMenuOption(GameOption.COMPUTER_SKILL_LEVEL, "Choose the skill level of the computer", [option for option in OnlineGameOptions.skill_level_options_dict]),
- MultiValueMenuOption(GameOption.COLOR, "Choose the side you would like to play as", [option for option in OnlineGameOptions.color_options]),
+ MultiValueMenuOption(GameOption.VARIANT, "Choose the variant to play", [option for option in OnlineGameOptions.variant_options_dict]), # noqa: E501
+ MultiValueMenuOption(GameOption.TIME_CONTROL, "Choose the time control", [option for option in OnlineGameOptions.time_control_options_dict]), # noqa: E501
+ MultiValueMenuOption(GameOption.COMPUTER_SKILL_LEVEL, "Choose the skill level of the computer", [option for option in OnlineGameOptions.skill_level_options_dict]), # noqa: E501
+ MultiValueMenuOption(GameOption.COLOR, "Choose the side you would like to play as", [option for option in OnlineGameOptions.color_options]), # noqa: E501
]
return MenuCategory("Play Online vs Computer", menu_options)
@@ -50,12 +50,12 @@ def _create_menu() -> MenuCategory:
"""Create the offline menu options"""
menu_options = [
# Todo: Implement ability to use custom or lichess fairy-stockfish defined strength levels
- MultiValueMenuOption(GameOption.VARIANT, "Choose the variant to play", [option for option in OfflineGameOptions.variant_options_dict]),
- MultiValueMenuOption(GameOption.TIME_CONTROL, "Choose the time control", [option for option in OfflineGameOptions.time_control_options_dict]),
+ MultiValueMenuOption(GameOption.VARIANT, "Choose the variant to play", [option for option in OfflineGameOptions.variant_options_dict]), # noqa: E501
+ MultiValueMenuOption(GameOption.TIME_CONTROL, "Choose the time control", [option for option in OfflineGameOptions.time_control_options_dict]), # noqa: E501
MultiValueMenuOption(GameOption.SPECIFY_ELO, "Would you like the computer to play as a specific Elo?", ["No", "Yes"]),
- MultiValueMenuOption(GameOption.COMPUTER_SKILL_LEVEL, "Choose the skill level of the computer", [option for option in OfflineGameOptions.skill_level_options_dict]),
+ MultiValueMenuOption(GameOption.COMPUTER_SKILL_LEVEL, "Choose the skill level of the computer", [option for option in OfflineGameOptions.skill_level_options_dict]), # noqa: E501
MultiValueMenuOption(GameOption.COMPUTER_ELO, "Choose the Elo of the computer", list(range(500, 2850)), visible=False),
- MultiValueMenuOption(GameOption.COLOR, "Choose the side you would like to play as", [option for option in OfflineGameOptions.color_options]),
+ MultiValueMenuOption(GameOption.COLOR, "Choose the side you would like to play as", [option for option in OfflineGameOptions.color_options]), # noqa: E501
]
return MenuCategory("Play Offline vs Computer", menu_options)
diff --git a/src/cli_chess/modules/board/board_model.py b/src/cli_chess/modules/board/board_model.py
index 95cb254..5f72648 100644
--- a/src/cli_chess/modules/board/board_model.py
+++ b/src/cli_chess/modules/board/board_model.py
@@ -273,7 +273,7 @@ def is_light_square(square: chess.Square) -> bool:
if square in chess.SQUARES:
return chess.BB_LIGHT_SQUARES & chess.BB_SQUARES[square]
else:
- raise(ValueError(f"Illegal square: {square}"))
+ raise ValueError(f"Illegal square: {square}")
def is_white_orientation(self) -> bool:
"""Returns True if the board orientation is set as white"""
diff --git a/src/cli_chess/modules/engine/engine_model.py b/src/cli_chess/modules/engine/engine_model.py
index 7cea861..af75ed4 100644
--- a/src/cli_chess/modules/engine/engine_model.py
+++ b/src/cli_chess/modules/engine/engine_model.py
@@ -40,7 +40,7 @@
async def create_engine_model(board_model: BoardModel, game_parameters: dict):
"""Create an instance of the engine model with the engine loaded"""
model = EngineModel(board_model, game_parameters)
- await model._init()
+ await model._init() # noqa
return model
diff --git a/src/cli_chess/modules/token_manager/token_manager_view.py b/src/cli_chess/modules/token_manager/token_manager_view.py
index ab870b3..63a2b4f 100644
--- a/src/cli_chess/modules/token_manager/token_manager_view.py
+++ b/src/cli_chess/modules/token_manager/token_manager_view.py
@@ -72,7 +72,7 @@ def _create_container(self) -> HSplit:
VSplit([
Label("Linked account: ", dont_extend_width=True),
ConditionalContainer(Label("None", style="class:label.error bold italic"), Condition(lambda: not self.lichess_username)),
- ConditionalContainer(Label(text=lambda: self.lichess_username, style="class:label.success bold"), Condition(lambda: self.lichess_username != "")),
+ ConditionalContainer(Label(text=lambda: self.lichess_username, style="class:label.success bold"), Condition(lambda: self.lichess_username != "")), # noqa: E501
], height=D(max=1)),
], width=D(max=self.container_width), height=D(preferred=8))
diff --git a/src/cli_chess/tests/modules/board/test_board_model.py b/src/cli_chess/tests/modules/board/test_board_model.py
index 0c2682d..086d159 100644
--- a/src/cli_chess/tests/modules/board/test_board_model.py
+++ b/src/cli_chess/tests/modules/board/test_board_model.py
@@ -15,7 +15,6 @@
from cli_chess.modules.board import BoardModel
import chess
-from chess import variant
from unittest.mock import Mock
import pytest
diff --git a/src/cli_chess/tests/modules/test_common.py b/src/cli_chess/tests/modules/test_common.py
index 4607fc1..a9a4a54 100644
--- a/src/cli_chess/tests/modules/test_common.py
+++ b/src/cli_chess/tests/modules/test_common.py
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from cli_chess.modules.common import *
+from cli_chess.modules.common import get_piece_unicode_symbol, UNICODE_PIECE_SYMBOLS
from string import ascii_lowercase
diff --git a/src/cli_chess/utils/config.py b/src/cli_chess/utils/config.py
index 7731c49..93273d2 100644
--- a/src/cli_chess/utils/config.py
+++ b/src/cli_chess/utils/config.py
@@ -78,7 +78,7 @@ def __init__(self, filename: str = DEFAULT_CONFIG_FILENAME) -> None:
# Event called on any configuration write event (across sections)
self.e_config_updated = Event()
- def _get_parser(self) -> "ConfigParser":
+ def _get_parser(self) -> "ConfigParser": # noqa: F821
"""Returns the config parser object"""
parser = configparser.ConfigParser()
parser.read(self.full_filename)
From c7ae9da9dbf93bc458150ee32bef790eec861f03 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Tue, 21 Feb 2023 15:35:32 -0600
Subject: [PATCH 03/25] Add ci.yml
---
.github/workflows/ci.yml | 66 ++++++++++++++++++++++++++++++++++++++
.github/workflows/test.yml | 41 -----------------------
2 files changed, 66 insertions(+), 41 deletions(-)
create mode 100644 .github/workflows/ci.yml
delete mode 100644 .github/workflows/test.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3b509fe
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,66 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ - development
+ pull_request:
+ branches:
+ - master
+ - development
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .[dev]
+
+ - name: Run flake8
+ run: |
+ echo "$PWD"
+ flake8 . --config=setup.cfg --count --show-source --statistics
+
+ test:
+ name: Test
+ needs: lint
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ python-version: ["3.7", "3.8", "3.9", "3.10"] # TODO: Add 3.11 after `wrapt` update: https://github.com/GrahamDumpleton/wrapt/issues/226
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .[dev]
+
+ - name: Run pytest
+ run: |
+ pytest
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index e6fc755..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Test
-
-on:
- push:
- branches:
- - development
- pull_request:
- branches:
- - master
-
-jobs:
- test:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: ["3.7", "3.8", "3.9", "3.10"] # TODO: Add 3.11 after `wrapt` update: https://github.com/GrahamDumpleton/wrapt/issues/226
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e .[dev]
-
- - name: Lint with flake8
- run: |
- # TODO: Add flake8 linting tests
- # stop the build if there are Python syntax errors or undefined names
- #flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- #flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
-
- - name: Run unit tests
- run: |
- pytest
From 5dfe4b47015e5a22524d710810ea06f36bd2e9cc Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Wed, 22 Feb 2023 08:58:42 -0600
Subject: [PATCH 04/25] Update .gitignore
---
.gitignore | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/.gitignore b/.gitignore
index 70b7651..cd419d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,16 +1,9 @@
config.ini
-*.swp
-*.~
-*.spec
+.coverage
*.pyc
*.egg-info
+*.spec
*.eggs/
-.coverage
-.venv/
-.vscode/
-.idea/
-venv/
dist/
build/
-engines/
__pycache__/
From e94dac91727fe63a534308bddc332f3a00a371c1 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Wed, 22 Feb 2023 09:35:14 -0600
Subject: [PATCH 05/25] Update README
---
README.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/README.md b/README.md
index d6d129f..cf083d6 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,15 @@ A highly customizable way to play chess in your terminal. Supports online play (
offline play against the Fairy-Stockfish engine. All Lichess variants are supported.
+
+
+
+
+
+
+
+
+
## Main Features
- Play online using your Lichess.org account
- Play offline against the Fairy-Stockfish engine
From 9a96f3caec64ca8d471479894f16b06e2dbe8d7d Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Thu, 23 Feb 2023 08:28:29 -0600
Subject: [PATCH 06/25] Update option order
---
src/cli_chess/menus/main_menu/main_menu_model.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/cli_chess/menus/main_menu/main_menu_model.py b/src/cli_chess/menus/main_menu/main_menu_model.py
index 1876db6..9df7fb7 100644
--- a/src/cli_chess/menus/main_menu/main_menu_model.py
+++ b/src/cli_chess/menus/main_menu/main_menu_model.py
@@ -18,8 +18,8 @@
class MainMenuOptions(Enum):
- ONLINE_GAMES = "Online Games"
OFFLINE_GAMES = "Offline Games"
+ ONLINE_GAMES = "Online Games"
SETTINGS = "Settings"
ABOUT = "About"
QUIT = "Quit"
@@ -34,8 +34,8 @@ def __init__(self):
def _create_menu() -> MenuCategory:
"""Create the menu category with options"""
menu_options = [
- MenuOption(MainMenuOptions.ONLINE_GAMES, "Play games online using Lichess.org"),
MenuOption(MainMenuOptions.OFFLINE_GAMES, "Play games offline"),
+ MenuOption(MainMenuOptions.ONLINE_GAMES, "Play games online using Lichess.org"),
MenuOption(MainMenuOptions.SETTINGS, "Modify cli-chess settings"),
MenuOption(MainMenuOptions.ABOUT, ""),
]
From 1dc2bda80bebb90b653778560c6bd8904ca10af2 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Thu, 23 Feb 2023 08:42:03 -0600
Subject: [PATCH 07/25] Set ELO step to 25
---
src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py b/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py
index ceef6e4..a665b60 100644
--- a/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py
+++ b/src/cli_chess/menus/vs_computer_menu/vs_computer_menu_model.py
@@ -49,12 +49,11 @@ def __init__(self):
def _create_menu() -> MenuCategory:
"""Create the offline menu options"""
menu_options = [
- # Todo: Implement ability to use custom or lichess fairy-stockfish defined strength levels
MultiValueMenuOption(GameOption.VARIANT, "Choose the variant to play", [option for option in OfflineGameOptions.variant_options_dict]), # noqa: E501
MultiValueMenuOption(GameOption.TIME_CONTROL, "Choose the time control", [option for option in OfflineGameOptions.time_control_options_dict]), # noqa: E501
MultiValueMenuOption(GameOption.SPECIFY_ELO, "Would you like the computer to play as a specific Elo?", ["No", "Yes"]),
MultiValueMenuOption(GameOption.COMPUTER_SKILL_LEVEL, "Choose the skill level of the computer", [option for option in OfflineGameOptions.skill_level_options_dict]), # noqa: E501
- MultiValueMenuOption(GameOption.COMPUTER_ELO, "Choose the Elo of the computer", list(range(500, 2850)), visible=False),
+ MultiValueMenuOption(GameOption.COMPUTER_ELO, "Choose the Elo of the computer", list(range(500, 2850, 25)), visible=False),
MultiValueMenuOption(GameOption.COLOR, "Choose the side you would like to play as", [option for option in OfflineGameOptions.color_options]), # noqa: E501
]
return MenuCategory("Play Offline vs Computer", menu_options)
From ae92b88a484ca526f64da6c065f5ea86d4387e46 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Fri, 24 Feb 2023 15:11:55 -0600
Subject: [PATCH 08/25] Remove comment
---
src/cli_chess/core/game/online_game/watch_tv/watch_tv_model.py | 2 --
1 file changed, 2 deletions(-)
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 6a1eaa9..777ecf2 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
@@ -98,8 +98,6 @@ def stream_event_received(self, **kwargs):
try:
# TODO: Data needs to be organized and sent to presenter to handle display
if 'startGameEvent' in kwargs:
- # NOTE: If the variant is 3check the initial export fen will include the check counts
- # but follow up game stream FENs will not. Lila GH issue #: 12357
event = kwargs['startGameEvent']
variant = event['variant']['key']
white_rating = int(event['players']['white'].get('rating') or 0)
From 2b23eb6c2c4bb8723d146111cf4498bf81a41fbc Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Fri, 24 Feb 2023 15:52:29 -0600
Subject: [PATCH 09/25] Adjust move list view
---
src/cli_chess/modules/move_list/move_list_view.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/cli_chess/modules/move_list/move_list_view.py b/src/cli_chess/modules/move_list/move_list_view.py
index 6ed70d6..dee71dd 100644
--- a/src/cli_chess/modules/move_list/move_list_view.py
+++ b/src/cli_chess/modules/move_list/move_list_view.py
@@ -37,8 +37,7 @@ def __init__(self, presenter: MoveListPresenter):
def _create_container(self) -> Box:
"""Create the move list container"""
return Box(self._move_list_output,
- width=D(min=1),
- height=D(min=1, max=4, preferred=4),
+ height=D(max=4),
padding=0)
def update(self, formatted_move_list: List[str]):
From 10c5601db1bc1f1ab5da5b29cdf2bd3b7b6ad4cb Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Fri, 24 Feb 2023 17:24:40 -0600
Subject: [PATCH 10/25] Clear event listeners on game completion
---
src/cli_chess/core/api/game_state_dispatcher.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/cli_chess/core/api/game_state_dispatcher.py b/src/cli_chess/core/api/game_state_dispatcher.py
index 3c9ea38..ff6b27b 100644
--- a/src/cli_chess/core/api/game_state_dispatcher.py
+++ b/src/cli_chess/core/api/game_state_dispatcher.py
@@ -47,10 +47,12 @@ def run(self):
self.e_game_state_dispatcher_event.notify(gameFull=event)
elif event['type'] == "gameState":
+ status = event.get('status', None)
+ is_game_over = status != None and status != "started" and status != "created"
+
self.e_game_state_dispatcher_event.notify(gameState=event)
- is_game_over = event.get('winner')
if is_game_over:
- break
+ self._game_ended()
elif event['type'] == "chatLine":
self.e_game_state_dispatcher_event.notify(chatLine=event)
@@ -97,6 +99,6 @@ def resign(self) -> None:
except Exception:
raise
- def clear_listeners(self) -> None:
- """Remove all event listeners"""
+ def _game_ended(self) -> None:
+ """Handles removing all event listeners since the game has completed"""
self.e_game_state_dispatcher_event.listeners.clear()
From 86b6c389e08a876795a6e5d2d7ce01aaff8b57a7 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Fri, 24 Feb 2023 17:25:44 -0600
Subject: [PATCH 11/25] Update game metadata status
---
src/cli_chess/core/game/game_model_base.py | 6 ++++--
.../core/game/online_game/online_game_model.py | 11 ++++-------
.../core/game/online_game/watch_tv/watch_tv_model.py | 4 ++--
3 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/src/cli_chess/core/game/game_model_base.py b/src/cli_chess/core/game/game_model_base.py
index 950b4e8..2f80c56 100644
--- a/src/cli_chess/core/game/game_model_base.py
+++ b/src/cli_chess/core/game/game_model_base.py
@@ -51,8 +51,6 @@ def _default_game_metadata() -> dict:
return {
'gameId': "",
'variant': "",
- 'winner': "",
- 'status': "",
'players': {
'white': {
'title': "",
@@ -79,6 +77,10 @@ def _default_game_metadata() -> dict:
'increment': 0
},
},
+ 'state': {
+ 'status': "",
+ 'winner': ""
+ }
}
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 86a4480..34dcb4f 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
@@ -69,12 +69,9 @@ def _game_end(self) -> None:
self.game_in_progress = False
self.playing_game_id = None
self._unsubscribe_from_iem_events()
- self.game_state_dispatcher.clear_listeners()
def handle_iem_event(self, **kwargs) -> None:
- """Handle event from the incoming event manager"""
- # TODO: Need to ensure IEM events we are responding to in this class are specific to this game being played.
- # Eg. We don't want to end the current game in progress, because one of our other correspondence games ended.
+ """Handles events received from the IncomingEventManager"""
if 'gameStart' in kwargs:
event = kwargs['gameStart']['game']
# TODO: There has to be a better way to ensure this is the right game...
@@ -86,11 +83,10 @@ def handle_iem_event(self, **kwargs) -> None:
elif 'gameFinish' in kwargs:
event = kwargs['gameFinish']['game']
if self.game_in_progress and self.playing_game_id == event['gameId']:
- self._save_game_metadata(iem_gameFinish=event)
self._game_end()
def handle_game_state_dispatcher_event(self, **kwargs) -> None:
- """Handle event from the game stream"""
+ """Handles received from the GameStateDispatcher"""
if 'gameFull' in kwargs:
event = kwargs['gameFull']
self._save_game_metadata(gsd_gameFull=event)
@@ -230,7 +226,8 @@ def _save_game_metadata(self, **kwargs) -> None:
# NOTE: Times below come from lichess in milliseconds
self.game_metadata['clock']['white']['time'] = data['wtime']
self.game_metadata['clock']['black']['time'] = data['btime']
- self.game_metadata['status'] = data['status']
+ self.game_metadata['state']['status'] = data.get('status')
+ self.game_metadata['state']['winner'] = data.get('winner', "") # Not included on draws or abort
self._notify_game_model_updated()
except Exception as e:
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 777ecf2..0c92f4b 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
@@ -77,8 +77,8 @@ def _save_game_metadata(self, **kwargs) -> None:
if 'tv_endGameEvent' in kwargs:
data = kwargs['tv_endGameEvent']
- self.game_metadata['status'] = data['status']
- self.game_metadata['winner'] = data.get('winner') # Not included on draws
+ self.game_metadata['state']['status'] = data.get('status')
+ self.game_metadata['state']['winner'] = data.get('winner') # Not included on draws or abort
for color in COLOR_NAMES:
if data['players'][color].get('user'):
From 271ec46b8703a288c0cc0fb1b690465cf4d08af0 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Sun, 26 Feb 2023 12:52:24 -0600
Subject: [PATCH 12/25] Add vulture
---
setup.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 20d2284..1e8f296 100644
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,8 @@
'dev': [
'pytest>=7.2.1,<8.0.0',
'pytest-cov>=4.0.0,<5.0.0',
- 'flake8>=5.0.4,<7.0.0'
+ 'flake8>=5.0.4,<7.0.0',
+ 'vulture>=2.7,<3.0'
]
}
From 211bccd0797cb6761ce0881fc05456eea325a5a3 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Sun, 26 Feb 2023 18:18:19 -0600
Subject: [PATCH 13/25] Update game_metadata dict
---
src/cli_chess/core/game/game_model_base.py | 7 +++----
.../core/game/offline_game/offline_game_model.py | 6 ++----
.../core/game/online_game/online_game_presenter.py | 3 +--
.../game/online_game/watch_tv/watch_tv_model.py | 2 +-
.../modules/player_info/player_info_presenter.py | 13 +++++--------
.../modules/player_info/player_info_view.py | 2 +-
6 files changed, 13 insertions(+), 20 deletions(-)
diff --git a/src/cli_chess/core/game/game_model_base.py b/src/cli_chess/core/game/game_model_base.py
index 2f80c56..85b4df3 100644
--- a/src/cli_chess/core/game/game_model_base.py
+++ b/src/cli_chess/core/game/game_model_base.py
@@ -12,7 +12,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-
from cli_chess.modules.board import BoardModel
from cli_chess.modules.move_list import MoveListModel
from cli_chess.modules.material_difference import MaterialDifferenceModel
@@ -56,14 +55,14 @@ def _default_game_metadata() -> dict:
'title': "",
'name': "",
'rating': "",
- 'ratingDiff': "",
+ 'rating_diff': "",
'provisional': False,
},
'black': {
'title': "",
'name': "",
'rating': "",
- 'ratingDiff': "",
+ 'rating_diff': "",
'provisional': False,
},
},
@@ -79,7 +78,7 @@ def _default_game_metadata() -> dict:
},
'state': {
'status': "",
- 'winner': ""
+ 'winner': "",
}
}
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 306262b..f9cb850 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
@@ -70,9 +70,7 @@ def _default_game_metadata(self) -> dict:
"""Returns the default structure for game metadata"""
game_metadata = super()._default_game_metadata()
game_metadata.update({
- 'my_color': "",
- 'rated': None,
- 'speed': None,
+ 'my_color_str': ""
})
return game_metadata
@@ -81,7 +79,7 @@ def _save_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_str'] = COLOR_NAMES[self.my_color]
self.game_metadata['variant'] = data[GameOption.VARIANT]
self.game_metadata['clock']['white']['time'] = data[GameOption.TIME_CONTROL][0]
self.game_metadata['clock']['white']['increment'] = data[GameOption.TIME_CONTROL][1]
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 5040de3..99e89e7 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
@@ -19,7 +19,7 @@
def start_online_game_vs_ai(game_parameters: dict) -> None:
- """Start a game vs the lichess ai"""
+ """Start a game vs the lichess AI"""
model = OnlineGameModel(game_parameters)
presenter = OnlineGamePresenter(model)
change_views(presenter.view, presenter.view.input_field_container) # noqa
@@ -28,7 +28,6 @@ def start_online_game_vs_ai(game_parameters: dict) -> None:
class OnlineGamePresenter(PlayableGamePresenterBase):
def __init__(self, model: OnlineGameModel):
- # NOTE: Model subscriptions are currently handled in parent. Override here if needed.
self.model = model
super().__init__(model)
self.view = OnlineGameView(self)
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 0c92f4b..956beba 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
@@ -84,7 +84,7 @@ def _save_game_metadata(self, **kwargs) -> None:
if data['players'][color].get('user'):
self.game_metadata['players'][color] = data['players'][color]['user']
self.game_metadata['players'][color]['rating'] = data['players'][color]['rating']
- self.game_metadata['players'][color]['ratingDiff'] = data.get('players', {}).get(color, {}).get('ratingDiff', "") # NOTE: Not included on aborted games # noqa
+ self.game_metadata['players'][color]['rating_diff'] = data.get('players', {}).get(color, {}).get('ratingDiff', "") # NOTE: Not included on aborted games # noqa
elif data['players'][color].get('aiLevel'):
self.game_metadata['players'][color]['name'] = f"Stockfish level {data['players'][color]['aiLevel']}"
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 39875e6..c7ac816 100644
--- a/src/cli_chess/modules/player_info/player_info_presenter.py
+++ b/src/cli_chess/modules/player_info/player_info_presenter.py
@@ -32,14 +32,11 @@ def __init__(self, model: GameModelBase):
self.model.e_game_model_updated.add_listener(self.update)
def update(self, **kwargs) -> None:
- """Updates the view based on orientation changes"""
- # TODO: Only respond to updated here if required (eg. based on kwarg)
- if 'board_orientation' in kwargs:
- pass
-
- orientation = self.model.board_model.get_board_orientation()
- self.view_upper.update(self.get_player_info(not orientation))
- self.view_lower.update(self.get_player_info(orientation))
+ """Updates the view based on specific model updates"""
+ if 'board_orientation' in kwargs or 'gameOver' in kwargs:
+ orientation = self.model.board_model.get_board_orientation()
+ 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) -> dict:
return self.model.game_metadata['players'][COLOR_NAMES[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 60dbaed..1cebcc5 100644
--- a/src/cli_chess/modules/player_info/player_info_view.py
+++ b/src/cli_chess/modules/player_info/player_info_view.py
@@ -53,7 +53,7 @@ def update(self, player_info: dict) -> None:
"""Updates the player info using the data passed in"""
self.player_title = player_info.get('title', "")
self.player_name = player_info.get('name', "Unknown")
- self._format_rating_diff(player_info.get('ratingDiff', None))
+ self._format_rating_diff(player_info.get('rating_diff', None))
rating = player_info.get('rating', "")
self.player_rating = (f"({rating})" if not player_info.get('provisional') else f"({rating}?)") if rating else ""
From b0e33de38a57af5c83acec695c8220592b2ca0ca Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Sun, 26 Feb 2023 18:18:54 -0600
Subject: [PATCH 14/25] Add event subscription methods
---
src/cli_chess/core/api/game_state_dispatcher.py | 9 +++++++--
src/cli_chess/core/api/incoming_event_manger.py | 9 +++++++++
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/cli_chess/core/api/game_state_dispatcher.py b/src/cli_chess/core/api/game_state_dispatcher.py
index ff6b27b..4c8c668 100644
--- a/src/cli_chess/core/api/game_state_dispatcher.py
+++ b/src/cli_chess/core/api/game_state_dispatcher.py
@@ -14,6 +14,7 @@
# along with this program. If not, see .
from cli_chess.utils import Event, log
+from typing import Callable
import threading
@@ -48,9 +49,9 @@ def run(self):
elif event['type'] == "gameState":
status = event.get('status', None)
- is_game_over = status != None and status != "started" and status != "created"
+ is_game_over = status and status != "started" and status != "created"
- self.e_game_state_dispatcher_event.notify(gameState=event)
+ self.e_game_state_dispatcher_event.notify(gameState=event, gameOver=is_game_over)
if is_game_over:
self._game_ended()
@@ -102,3 +103,7 @@ def resign(self) -> None:
def _game_ended(self) -> None:
"""Handles removing all event listeners since the game has completed"""
self.e_game_state_dispatcher_event.listeners.clear()
+
+ def subscribe_to_events(self, listener: Callable) -> None:
+ """Subscribes the passed in method to GSD events"""
+ self.e_game_state_dispatcher_event.add_listener(listener)
diff --git a/src/cli_chess/core/api/incoming_event_manger.py b/src/cli_chess/core/api/incoming_event_manger.py
index 82ddeed..182a6b5 100644
--- a/src/cli_chess/core/api/incoming_event_manger.py
+++ b/src/cli_chess/core/api/incoming_event_manger.py
@@ -15,6 +15,7 @@
from cli_chess.utils.event import Event
from cli_chess.utils.logging import log
+from typing import Callable
import threading
@@ -77,3 +78,11 @@ def run(self) -> None:
def get_active_games(self) -> list:
"""Returns a list of games in progress for this account"""
return self.my_games
+
+ def subscribe_to_iem_events(self, listener: Callable) -> None:
+ """Subscribes the passed in method to IEM events"""
+ self.e_new_event_received.add_listener(listener)
+
+ def unsubscribe_from_iem_events(self, listener: Callable) -> None:
+ """Unsubscribes the passed in method to IEM events"""
+ self.e_new_event_received.add_listener(listener)
From 6b543856610287a6095b71f7eec32a89c1ad51ed Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Sun, 26 Feb 2023 18:20:05 -0600
Subject: [PATCH 15/25] Initial game over result logic
---
.../core/game/game_presenter_base.py | 47 ++++++++++++++++-
.../game/online_game/online_game_model.py | 52 +++++++++----------
2 files changed, 69 insertions(+), 30 deletions(-)
diff --git a/src/cli_chess/core/game/game_presenter_base.py b/src/cli_chess/core/game/game_presenter_base.py
index 98493b5..ee86448 100644
--- a/src/cli_chess/core/game/game_presenter_base.py
+++ b/src/cli_chess/core/game/game_presenter_base.py
@@ -19,6 +19,8 @@
from cli_chess.modules.move_list import MoveListPresenter
from cli_chess.modules.material_difference import MaterialDifferencePresenter
from cli_chess.modules.player_info import PlayerInfoPresenter
+from cli_chess.utils.logging import log
+from chess import Color, COLOR_NAMES
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@@ -54,14 +56,55 @@ def exit(self) -> None:
class PlayableGamePresenterBase(GamePresenterBase):
def __init__(self, model: PlayableGameModelBase):
- # NOTE: Base Model subscriptions are currently handled in parent.
- # Override the update function here if needed.
self.model = model
super().__init__(model)
self.view = PlayableGameViewBase(self)
self.model.board_model.e_successful_move_made.add_listener(self.view.clear_error)
+ def update(self, **kwargs) -> None:
+ """Overrides base and responds to specific model updates"""
+ if 'gameOver' in kwargs:
+ self._parse_and_present_game_over()
+
+ def _parse_and_present_game_over(self):
+ """Handles parsing and presenting the game over status"""
+ if not self.is_game_in_progress():
+ status = self.model.game_metadata['state']['status']
+ winner = self.model.game_metadata['state']['winner'].capitalize()
+
+ status_win_reasons = ['mate', 'resign', 'timeout', 'outoftime', 'cheat', 'variantEnd']
+ if winner and status in status_win_reasons:
+ output = f" • {winner} is victorious"
+ loser = COLOR_NAMES[not Color(COLOR_NAMES.index(winner.lower()))].capitalize()
+
+ if status == "mate":
+ output = "Checkmate" + output
+ elif status == "resign":
+ output = f"{loser} resigned" + output
+ elif status == "timeout":
+ output = f"{loser} left the game" + output
+ elif status == "outoftime":
+ output = f"{loser} time out" + output
+ elif status == "cheat":
+ output = "Cheat detected"
+ else: # TODO: Handle variantEnd (need to know variant)
+ log.debug(f"PlayableGamePresenterBase: Received game over with uncaught status: {status} / {winner}")
+ output = "Game over" + output
+
+ else: # Handle other game end reasons
+ if status == "aborted":
+ output = "Game aborted"
+ elif status == "draw":
+ output = "Game over • Draw"
+ elif status == "stalemate":
+ output = "Game over • Stalemate"
+ else:
+ log.debug(f"PlayableGamePresenterBase: Received game over with uncaught status: {status}")
+ output = "Game over"
+
+ self.view.show_error(output)
+
def user_input_received(self, inpt: str) -> None:
"""Respond to the users input. This input can either be the
move input, or game actions (such as resign)
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 34dcb4f..9d2a3da 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
@@ -17,7 +17,7 @@
from cli_chess.modules.game_options import GameOption
from cli_chess.core.api import GameStateDispatcher
from cli_chess.utils import log, threaded
-from chess import Color, COLOR_NAMES
+from chess import COLOR_NAMES
from typing import Optional
@@ -44,12 +44,12 @@ def __init__(self, game_parameters: dict):
@threaded
def start_ai_challenge(self) -> None:
"""Sends a request to lichess to start an AI challenge using the selected game parameters"""
- # Note: Only add IEM listener right before creating event to lessen chance of grabbing another game
- self._subscribe_to_iem_events()
+ # Note: Only subscribe to IEM events right before creating challenge to lessen chance of grabbing another game
+ self.api_iem.subscribe_to_iem_events(self.handle_iem_event)
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'],
+ color=self.game_metadata['my_color_str'],
variant=self.game_metadata['variant'])
def _start_game(self, game_id: str) -> None:
@@ -61,14 +61,14 @@ def _start_game(self, game_id: str) -> None:
self.playing_game_id = game_id
self.game_state_dispatcher = GameStateDispatcher(game_id)
- self.game_state_dispatcher.e_game_state_dispatcher_event.add_listener(self.handle_game_state_dispatcher_event)
+ self.game_state_dispatcher.subscribe_to_events(self.handle_game_state_dispatcher_event)
self.game_state_dispatcher.start()
def _game_end(self) -> None:
"""The game we are playing has ended. Handle cleaning up."""
self.game_in_progress = False
self.playing_game_id = None
- self._unsubscribe_from_iem_events()
+ self.api_iem.unsubscribe_from_iem_events(self.handle_iem_event)
def handle_iem_event(self, **kwargs) -> None:
"""Handles events received from the IncomingEventManager"""
@@ -90,7 +90,6 @@ def handle_game_state_dispatcher_event(self, **kwargs) -> None:
if 'gameFull' in kwargs:
event = kwargs['gameFull']
self._save_game_metadata(gsd_gameFull=event)
- self.my_color = Color(COLOR_NAMES.index(self.game_metadata['my_color']))
self.board_model.reinitialize_board(variant=self.game_metadata['variant'],
orientation=self.my_color,
fen=event['initialFen'])
@@ -106,6 +105,9 @@ def handle_game_state_dispatcher_event(self, **kwargs) -> None:
self.board_model.reset(notify=False)
self.board_model.make_moves_from_list(event['moves'].split())
+ if kwargs['gameOver']:
+ self._report_game_over(status=event.get('status'), winner=event.get('winner', ""))
+
elif 'chatLine' in kwargs:
event = kwargs['chatLine']
self._save_game_metadata(gsd_chatLine=event)
@@ -177,17 +179,14 @@ def resign(self) -> None:
raise Warning("Game has already ended")
def _save_game_metadata(self, **kwargs) -> None:
- """Parses and saves the data of the game being played.
- If notify is false, a model update notification will not be sent.
- Raises an exception on invalid data.
- """
+ """Parses and saves the data of the game being played."""
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'] = data[GameOption.COLOR]
+ self.game_metadata['my_color_str'] = COLOR_NAMES[self.my_color]
self.game_metadata['variant'] = data[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['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[GameOption.TIME_CONTROL][0] * 60 # secs
self.game_metadata['clock']['white']['increment'] = data[GameOption.TIME_CONTROL][1] # secs
self.game_metadata['clock']['black'] = self.game_metadata['clock']['white']
@@ -198,7 +197,7 @@ def _save_game_metadata(self, **kwargs) -> None:
data = kwargs['iem_gameStart']
self.game_metadata['gameId'] = data['gameId']
- self.game_metadata['my_color'] = data['color']
+ self.game_metadata['my_color_str'] = data['color']
self.game_metadata['rated'] = data['rated']
self.game_metadata['variant'] = data['variant']['name']
self.game_metadata['speed'] = data['speed']
@@ -226,31 +225,28 @@ def _save_game_metadata(self, **kwargs) -> None:
# NOTE: Times below come from lichess in milliseconds
self.game_metadata['clock']['white']['time'] = data['wtime']
self.game_metadata['clock']['black']['time'] = data['btime']
- self.game_metadata['state']['status'] = data.get('status')
- self.game_metadata['state']['winner'] = data.get('winner', "") # Not included on draws or abort
self._notify_game_model_updated()
except Exception as e:
log.exception(f"Error saving online game metadata: {e}")
raise
- def _subscribe_to_iem_events(self) -> None:
- """Subscribe to IEM events by adding a handler"""
- self.api_iem.e_new_event_received.add_listener(self.handle_iem_event)
-
- def _unsubscribe_from_iem_events(self) -> None:
- """Unsubscribe from IEM events. It's important that this happens after a game
- completes, or the user quits otherwise IEM subscriptions will build up.
- """
- self.api_iem.e_new_event_received.remove_listener(self.handle_iem_event)
-
def _default_game_metadata(self) -> dict:
"""Returns the default structure for game metadata"""
game_metadata = super()._default_game_metadata()
game_metadata.update({
- 'my_color': "",
+ 'my_color_str': "",
'ai_level': None,
'rated': False,
'speed': None,
})
return game_metadata
+
+ def _report_game_over(self, status: str, winner: str) -> None:
+ """Saves game information and notifies listeners that the game has ended.
+ 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._notify_game_model_updated(gameOver=True)
From 3b7f18ffb61c240d4e808bb163e473b70512e6e2 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Mon, 27 Feb 2023 08:52:47 -0600
Subject: [PATCH 16/25] Bump pt version
---
setup.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 1e8f296..a2aa638 100644
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,9 @@
dependencies = [
"chess>=1.9.4,<2.0.0",
"berserk-downstream>=0.11.12,<1.0.0",
- "prompt-toolkit>=3.0.36,<4.0.0"
+ "prompt-toolkit==3.0.38" # pin as breaking changes have been
+ # introduced in previous patch versions
+ # read PT changelog before bumping
]
dev_dependencies = {
From ec5549c546b4855368e16cb68b1d5e4ba2786b23 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Tue, 28 Feb 2023 12:26:20 -0600
Subject: [PATCH 17/25] Thread token validation
---
.../modules/token_manager/token_manager_model.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/cli_chess/modules/token_manager/token_manager_model.py b/src/cli_chess/modules/token_manager/token_manager_model.py
index 6d2fcdc..ddcff39 100644
--- a/src/cli_chess/modules/token_manager/token_manager_model.py
+++ b/src/cli_chess/modules/token_manager/token_manager_model.py
@@ -13,11 +13,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from cli_chess.utils.logging import log
from cli_chess.utils.config import lichess_config
-from cli_chess.utils.event import Event
+from cli_chess.utils import Event, log, threaded
from berserk import Client, TokenSession
-from berserk.exceptions import BerserkError
linked_token_scopes = set()
@@ -27,6 +25,7 @@ def __init__(self):
self.linked_account = ""
self.e_token_manager_model_updated = Event()
+ @threaded
def validate_existing_linked_account(self) -> None:
"""Queries the Lichess config file for an existing token. If a token
exists, verification is attempted. Invalid data will be cleared.
@@ -78,8 +77,9 @@ def validate_token(api_token: str) -> dict:
return token_data[api_token]
else:
log.error("TokenManager: Valid token but missing required scopes")
- except BerserkError as e:
- log.error(f"TokenManager: Authentication to Lichess failed - {e.message}")
+
+ except Exception as e:
+ log.error(f"TokenManager: Authentication to Lichess failed - {e}")
def save_account_data(self, api_token: str, account_data: dict, valid=False) -> None:
"""Saves the passed in lichess api token to the configuration.
From 689a13b8fee110bae5b9bcfbd15c593fb46ef606 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Tue, 28 Feb 2023 12:27:06 -0600
Subject: [PATCH 18/25] Set threaded wrapper as daemon
---
src/cli_chess/utils/common.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/cli_chess/utils/common.py b/src/cli_chess/utils/common.py
index 78a11b8..504c790 100644
--- a/src/cli_chess/utils/common.py
+++ b/src/cli_chess/utils/common.py
@@ -43,7 +43,7 @@ def str_to_bool(s: str) -> bool:
def threaded(fn):
"""Decorator for a threaded function"""
def wrapper(*args, **kwargs):
- threading.Thread(target=fn, args=args, kwargs=kwargs).start()
+ threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
From 964f7f1033ef900eddcc8da68be9a3bf4b48cfbb Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Tue, 28 Feb 2023 14:28:58 -0600
Subject: [PATCH 19/25] Dynamically create API URL
---
src/cli_chess/core/api/api_manager.py | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/src/cli_chess/core/api/api_manager.py b/src/cli_chess/core/api/api_manager.py
index 579961e..b2fbf43 100644
--- a/src/cli_chess/core/api/api_manager.py
+++ b/src/cli_chess/core/api/api_manager.py
@@ -18,14 +18,7 @@
from berserk import Client, TokenSession
from typing import Optional
-API_TOKEN_CREATION_URL = ("https://lichess.org/account/oauth/token/create?" +
- "scopes[]=challenge:read&" +
- "scopes[]=challenge:write&" +
- "scopes[]=board:play&" +
- "description=cli-chess+token")
-
required_token_scopes: set = {"board:play", "challenge:read", "challenge:write"}
-
api_session: Optional[TokenSession]
api_client: Optional[Client]
api_iem: Optional[IncomingEventManager]
@@ -54,3 +47,17 @@ def api_is_ready() -> bool:
this is used for toggling the online menu availability
"""
return api_ready
+
+
+def _create_api_token_url() -> str:
+ """Created the API token creation url by iterating over scopes"""
+ url = "https://lichess.org/account/oauth/token/create?"
+
+ for scope in required_token_scopes:
+ url = url + f"scopes[]={scope}&"
+
+ url = url + "description=cli-chess+token"
+ return url
+
+
+API_TOKEN_CREATION_URL = _create_api_token_url()
From 3b3c6eb593fc71e00311a7e9d299cabad71b3202 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Wed, 1 Mar 2023 11:39:03 -0600
Subject: [PATCH 20/25] Fix unsubscribe method
---
src/cli_chess/core/api/incoming_event_manger.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/cli_chess/core/api/incoming_event_manger.py b/src/cli_chess/core/api/incoming_event_manger.py
index 182a6b5..42c6446 100644
--- a/src/cli_chess/core/api/incoming_event_manger.py
+++ b/src/cli_chess/core/api/incoming_event_manger.py
@@ -85,4 +85,4 @@ def subscribe_to_iem_events(self, listener: Callable) -> None:
def unsubscribe_from_iem_events(self, listener: Callable) -> None:
"""Unsubscribes the passed in method to IEM events"""
- self.e_new_event_received.add_listener(listener)
+ self.e_new_event_received.remove_listener(listener)
From 09f4d60234aff5ade229c5faf1480b7a07d9c4dd Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Wed, 1 Mar 2023 19:17:49 -0600
Subject: [PATCH 21/25] Update tests
---
.../token_manager/test_token_manager_model.py | 24 -------------------
1 file changed, 24 deletions(-)
diff --git a/src/cli_chess/tests/modules/token_manager/test_token_manager_model.py b/src/cli_chess/tests/modules/token_manager/test_token_manager_model.py
index 0cd80e2..f4e9f34 100644
--- a/src/cli_chess/tests/modules/token_manager/test_token_manager_model.py
+++ b/src/cli_chess/tests/modules/token_manager/test_token_manager_model.py
@@ -56,30 +56,6 @@ def mock_success_test_tokens(*args): # noqa
}
-def test_validate_existing_linked_account(model: TokenManagerModel, lichess_config: LichessConfig, model_listener: Mock, monkeypatch):
- # Test with empty API token
- lichess_config.set_value(lichess_config.Keys.API_TOKEN, "")
- model.validate_existing_linked_account()
- assert lichess_config.get_value(lichess_config.Keys.API_TOKEN) == ""
-
- # Test with invalid existing API token
- monkeypatch.setattr(clients.OAuth, "test_tokens", mock_fail_test_tokens)
- lichess_config.set_value(lichess_config.Keys.API_TOKEN, "lip_badToken")
- model.validate_existing_linked_account()
- assert lichess_config.get_value(lichess_config.Keys.API_TOKEN) == ""
-
- # Test with valid API token
- monkeypatch.setattr(clients.OAuth, "test_tokens", mock_success_test_tokens)
- lichess_config.set_value(lichess_config.Keys.API_TOKEN, "lip_validToken")
- model.validate_existing_linked_account()
- assert lichess_config.get_value(lichess_config.Keys.API_TOKEN) == "lip_validToken"
-
- # Verify model listener is called
- model_listener.reset_mock()
- model.validate_existing_linked_account()
- model_listener.assert_called()
-
-
def test_update_linked_account(model: TokenManagerModel, lichess_config: LichessConfig, model_listener: Mock, monkeypatch):
# Test with empty api token
assert not model.update_linked_account(api_token="")
From 9c3cd6a9cb55aaef6b4908b36071233fcfdae349 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Wed, 1 Mar 2023 19:22:34 -0600
Subject: [PATCH 22/25] Fix persistent event subscriptions
---
.../core/api/game_state_dispatcher.py | 2 +-
src/cli_chess/core/game/game_model_base.py | 22 ++++++++++--
.../core/game/game_presenter_base.py | 1 +
.../online_game/watch_tv/watch_tv_model.py | 1 +
src/cli_chess/modules/board/board_model.py | 15 +++++---
.../material_difference_model.py | 11 ++++--
.../modules/move_list/move_list_model.py | 11 ++++--
src/cli_chess/utils/__init__.py | 2 +-
src/cli_chess/utils/event.py | 35 ++++++++++++++++---
9 files changed, 84 insertions(+), 16 deletions(-)
diff --git a/src/cli_chess/core/api/game_state_dispatcher.py b/src/cli_chess/core/api/game_state_dispatcher.py
index 4c8c668..cc77fa1 100644
--- a/src/cli_chess/core/api/game_state_dispatcher.py
+++ b/src/cli_chess/core/api/game_state_dispatcher.py
@@ -102,7 +102,7 @@ def resign(self) -> None:
def _game_ended(self) -> None:
"""Handles removing all event listeners since the game has completed"""
- self.e_game_state_dispatcher_event.listeners.clear()
+ self.e_game_state_dispatcher_event.remove_all_listeners()
def subscribe_to_events(self, listener: Callable) -> None:
"""Subscribes the passed in method to GSD events"""
diff --git a/src/cli_chess/core/game/game_model_base.py b/src/cli_chess/core/game/game_model_base.py
index 85b4df3..98cdeba 100644
--- a/src/cli_chess/core/game/game_model_base.py
+++ b/src/cli_chess/core/game/game_model_base.py
@@ -12,10 +12,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+
from cli_chess.modules.board import BoardModel
from cli_chess.modules.move_list import MoveListModel
from cli_chess.modules.material_difference import MaterialDifferenceModel
-from cli_chess.utils.event import Event
+from cli_chess.utils import EventManager, log
from chess import Color, WHITE, COLOR_NAMES
from random import getrandbits
from abc import ABC, abstractmethod
@@ -29,9 +30,13 @@ def __init__(self, orientation: Color = WHITE, variant="standard", fen=""):
self.move_list_model = MoveListModel(self.board_model)
self.material_diff_model = MaterialDifferenceModel(self.board_model)
- self.e_game_model_updated = Event()
+ self._event_manager = EventManager()
+ self.e_game_model_updated = self._event_manager.create_event()
self.board_model.e_board_model_updated.add_listener(self.update)
+ # Keep track of all associated models to handle bulk cleanup on exit
+ self._assoc_models = [self.board_model, self.move_list_model, self.material_diff_model]
+
def update(self, **kwargs) -> None:
"""Called automatically as part of an event listener. This function
listens to model update events and if deemed necessary triages
@@ -40,6 +45,19 @@ def update(self, **kwargs) -> None:
if 'board_orientation' in kwargs:
self._notify_game_model_updated(**kwargs)
+ def cleanup(self) -> None:
+ """Cleans up after this model by clearing all associated models event listeners.
+ This should only ever be run when the models are no longer needed.
+ """
+ self._event_manager.purge_all_events()
+
+ # Notify associated models to clean up
+ for model in self._assoc_models:
+ try:
+ model.cleanup()
+ except AttributeError:
+ log.error(f"GameModelBase: {model} does not have a cleanup method")
+
def _notify_game_model_updated(self, **kwargs) -> None:
"""Notify listeners that the model has updated"""
self.e_game_model_updated.notify(**kwargs)
diff --git a/src/cli_chess/core/game/game_presenter_base.py b/src/cli_chess/core/game/game_presenter_base.py
index ee86448..c021b73 100644
--- a/src/cli_chess/core/game/game_presenter_base.py
+++ b/src/cli_chess/core/game/game_presenter_base.py
@@ -51,6 +51,7 @@ def flip_board(self) -> None:
def exit(self) -> None:
"""Exit current presenter/view"""
+ self.model.cleanup()
self.view.exit()
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 956beba..a5f87dc 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
@@ -243,4 +243,5 @@ def stop_watching(self):
# TODO: Need to handle going back to the main menu when the TVStream
# connection retries are exhausted. Send event notification to model?
log.info("TV Stream: Stopping TV stream")
+ self.e_tv_stream_event.remove_all_listeners()
self.running = False
diff --git a/src/cli_chess/modules/board/board_model.py b/src/cli_chess/modules/board/board_model.py
index 5f72648..9897963 100644
--- a/src/cli_chess/modules/board/board_model.py
+++ b/src/cli_chess/modules/board/board_model.py
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from cli_chess.utils.event import Event
+from cli_chess.utils.event import EventManager
from cli_chess.utils.logging import log
from random import randint
from typing import List
@@ -27,10 +27,11 @@ def __init__(self, orientation: chess.Color = chess.WHITE, variant="standard", f
self.initial_fen = self.board.fen()
self.orientation = chess.WHITE if variant.lower() == "racingkings" else orientation
self.highlight_move = chess.Move.null()
-
self._log_init_info()
- self.e_board_model_updated = Event()
- self.e_successful_move_made = Event()
+
+ self._event_manager = EventManager()
+ self.e_board_model_updated = self._event_manager.create_event()
+ self.e_successful_move_made = self._event_manager.create_event()
@staticmethod
def _initialize_board(variant: str, fen: str):
@@ -304,6 +305,12 @@ def _notify_successful_move_made(self) -> None:
"""Notifies listeners that a board move has been made"""
self.e_successful_move_made.notify()
+ def cleanup(self) -> None:
+ """Handles model cleanup tasks. This should only ever
+ be run when this model is no longer needed.
+ """
+ self._event_manager.purge_all_events()
+
def _log_init_info(self):
"""Logs class initialization"""
log.debug("=============== BOARD INITIALIZATION ===============")
diff --git a/src/cli_chess/modules/material_difference/material_difference_model.py b/src/cli_chess/modules/material_difference/material_difference_model.py
index cb893e0..08ca6a4 100644
--- a/src/cli_chess/modules/material_difference/material_difference_model.py
+++ b/src/cli_chess/modules/material_difference/material_difference_model.py
@@ -14,7 +14,7 @@
# along with this program. If not, see .
from cli_chess.modules.board import BoardModel
-from cli_chess.utils import Event
+from cli_chess.utils import EventManager
from typing import Dict
from chess import PIECE_SYMBOLS, PIECE_TYPES, PieceType, Color, COLORS, WHITE, BLACK, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING
import re
@@ -37,7 +37,8 @@ def __init__(self, board_model: BoardModel):
self.material_difference: Dict[Color, Dict[PieceType, int]] = self.default_material_difference()
self.score: Dict[Color, int] = self.default_score()
- self.e_material_difference_model_updated = Event()
+ self._event_manager = EventManager()
+ self.e_material_difference_model_updated = self._event_manager.create_event()
self.update()
@staticmethod
@@ -142,3 +143,9 @@ def get_board_orientation(self) -> Color:
def _notify_material_difference_model_updated(self) -> None:
"""Notifies listeners of material difference model updates"""
self.e_material_difference_model_updated.notify()
+
+ def cleanup(self) -> None:
+ """Handles model cleanup tasks. This should only ever
+ be run when this model is no longer needed.
+ """
+ self._event_manager.purge_all_events()
diff --git a/src/cli_chess/modules/move_list/move_list_model.py b/src/cli_chess/modules/move_list/move_list_model.py
index 1db69ce..6906055 100644
--- a/src/cli_chess/modules/move_list/move_list_model.py
+++ b/src/cli_chess/modules/move_list/move_list_model.py
@@ -14,7 +14,7 @@
# along with this program. If not, see .
from cli_chess.modules.board import BoardModel
-from cli_chess.utils import Event, log
+from cli_chess.utils import EventManager, log
from chess import piece_symbol
from typing import List
@@ -25,7 +25,8 @@ def __init__(self, board_model: BoardModel) -> None:
self.board_model.e_board_model_updated.add_listener(self.update)
self.move_list_data = []
- self.e_move_list_model_updated = Event()
+ self._event_manager = EventManager()
+ self.e_move_list_model_updated = self._event_manager.create_event()
self.update()
def update(self, **kwargs) -> None: # noqa
@@ -68,3 +69,9 @@ def get_move_list_data(self) -> List[dict]:
def _notify_move_list_model_updated(self) -> None:
"""Notifies listeners of move list model updates"""
self.e_move_list_model_updated.notify()
+
+ def cleanup(self) -> None:
+ """Handles model cleanup tasks. This should only ever
+ be run when this model is no longer needed.
+ """
+ self._event_manager.purge_all_events()
diff --git a/src/cli_chess/utils/__init__.py b/src/cli_chess/utils/__init__.py
index a34d6ae..0f72ef0 100644
--- a/src/cli_chess/utils/__init__.py
+++ b/src/cli_chess/utils/__init__.py
@@ -1,6 +1,6 @@
from .common import is_linux_os, is_windows_os, is_mac_os, str_to_bool, threaded, open_url_in_browser
from .config import force_recreate_configs, print_program_config
-from .event import Event
+from .event import Event, EventManager
from .logging import log, redact_from_logs
from .argparse import setup_argparse
from .styles import default, twilight
diff --git a/src/cli_chess/utils/event.py b/src/cli_chess/utils/event.py
index 151d8d0..754e652 100644
--- a/src/cli_chess/utils/event.py
+++ b/src/cli_chess/utils/event.py
@@ -14,13 +14,14 @@
# along with this program. If not, see .
from __future__ import annotations
-from typing import Callable
+from typing import Callable, List
class Event:
- """Event notification class. Interested listeners can add a callable
- to be notified when the event is triggered (using notify()). Generally
- this is used for models to notify presenters of updated data.
+ """Event notification class. This class creates a singular event instance
+ which listeners can subscribe to with a callable. The callable will be
+ notified when the event is triggered (using notify()). Generallty, this
+ class should not be instantiated directly, but rather from the EventManager class.
"""
def __init__(self):
self.listeners = []
@@ -35,7 +36,33 @@ def remove_listener(self, listener: Callable) -> None:
if listener in self.listeners:
self.listeners.remove(listener)
+ def remove_all_listeners(self) -> None:
+ """Removes all listeners associated to this event"""
+ self.listeners.clear()
+
def notify(self, *args, **kwargs) -> None:
"""Notifies all listeners of the event"""
for listener in self.listeners:
listener(*args, **kwargs)
+
+
+class EventManager:
+ """Event manager class. Models which use events should create
+ events using this manager for easier event maintenance
+ """
+ def __init__(self):
+ self._event_list: List[Event] = []
+
+ def create_event(self) -> Event:
+ """Creates and returns a new event for listeners to subscribe to"""
+ e = Event()
+ self._event_list.append(e)
+ return e
+
+ def purge_all_events(self) -> None:
+ """Purges all events in the event list by removing
+ all associated events/listeners
+ """
+ for event in self._event_list:
+ event.remove_all_listeners()
+ self._event_list.clear()
From b7ae03a8a24ce2483bd2b6637a6cc70306c7cc18 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Thu, 2 Mar 2023 09:32:30 -0600
Subject: [PATCH 23/25] Add ability to purge all listeners
---
src/cli_chess/utils/event.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/src/cli_chess/utils/event.py b/src/cli_chess/utils/event.py
index 754e652..8ee9f95 100644
--- a/src/cli_chess/utils/event.py
+++ b/src/cli_chess/utils/event.py
@@ -59,10 +59,16 @@ def create_event(self) -> Event:
self._event_list.append(e)
return e
- def purge_all_events(self) -> None:
- """Purges all events in the event list by removing
- all associated events/listeners
+ def purge_all_event_listeners(self) -> None:
+ """For each event associated to this event manager
+ this method will clear all listeners
"""
for event in self._event_list:
event.remove_all_listeners()
+
+ def purge_all_events(self) -> None:
+ """Purges all events in the event list by removing
+ all associated events and listeners
+ """
+ self.purge_all_event_listeners()
self._event_list.clear()
From 0df7fd0e3bc312a66d8c2676675ab6baab4d6a47 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Thu, 2 Mar 2023 09:33:04 -0600
Subject: [PATCH 24/25] Add tests for EventManager
---
src/cli_chess/tests/utils/test_event.py | 147 ++++++++++++++++--------
1 file changed, 96 insertions(+), 51 deletions(-)
diff --git a/src/cli_chess/tests/utils/test_event.py b/src/cli_chess/tests/utils/test_event.py
index ff1b6ea..31279b8 100644
--- a/src/cli_chess/tests/utils/test_event.py
+++ b/src/cli_chess/tests/utils/test_event.py
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from cli_chess.utils import Event
+from cli_chess.utils import Event, EventManager
from unittest.mock import Mock
import pytest
@@ -35,53 +35,98 @@ def event(listener1):
return event
-def test_add_listener(event: Event, listener1: Mock, listener2: Mock):
- event.add_listener(listener2)
- assert listener2 in event.listeners
-
- event.add_listener(listener1)
- assert event.listeners.count(listener1) == 1
-
-
-def test_remove_listener(event: Event, listener1: Mock, listener2: Mock):
- assert listener2 not in event.listeners
- event.remove_listener(listener2)
- assert listener1 in event.listeners
-
- event.add_listener(listener2)
- event.remove_listener(listener1)
- assert listener1 not in event.listeners
- assert listener2 in event.listeners
-
-
-def test_notify(event: Event, listener1: Mock, listener2: Mock):
- listener1.assert_not_called()
- listener2.assert_not_called()
-
- event.notify()
- listener1.assert_called()
- listener2.assert_not_called()
-
- # Test notification after adding a listener
- listener1.reset_mock()
- event.add_listener(listener2)
- event.notify()
- listener1.assert_called()
- listener2.assert_called()
-
- # Test notification after removing a listener
- listener1.reset_mock()
- listener2.reset_mock()
- event.remove_listener(listener1)
- event.notify()
- listener1.assert_not_called()
- listener2.assert_called()
-
- # Try notifying without any listeners
- listener1.reset_mock()
- listener2.reset_mock()
- event.listeners.clear()
- assert not event.listeners
- event.notify()
- listener1.assert_not_called()
- listener2.assert_not_called()
+@pytest.fixture
+def event_manager():
+ event_manger = EventManager()
+ event_manger.create_event().add_listener(listener1)
+ return event_manger
+
+
+class TestEvent:
+ def test_add_listener(self, event: Event, listener1: Mock, listener2: Mock):
+ event.add_listener(listener2)
+ assert listener2 in event.listeners
+
+ event.add_listener(listener1)
+ assert event.listeners.count(listener1) == 1
+
+ def test_remove_listener(self, event: Event, listener1: Mock, listener2: Mock):
+ assert listener2 not in event.listeners
+ event.remove_listener(listener2)
+ assert listener1 in event.listeners
+
+ event.add_listener(listener2)
+ event.remove_listener(listener1)
+ assert listener1 not in event.listeners
+ assert listener2 in event.listeners
+
+ def test_notify(self, event: Event, listener1: Mock, listener2: Mock):
+ listener1.assert_not_called()
+ listener2.assert_not_called()
+
+ event.notify()
+ listener1.assert_called()
+ listener2.assert_not_called()
+
+ # Test notification after adding a listener
+ listener1.reset_mock()
+ event.add_listener(listener2)
+ event.notify()
+ listener1.assert_called()
+ listener2.assert_called()
+
+ # Test notification after removing a listener
+ listener1.reset_mock()
+ listener2.reset_mock()
+ event.remove_listener(listener1)
+ event.notify()
+ listener1.assert_not_called()
+ listener2.assert_called()
+
+ # Try notifying without any listeners
+ listener1.reset_mock()
+ listener2.reset_mock()
+ event.listeners.clear()
+ assert not event.listeners
+ event.notify()
+ listener1.assert_not_called()
+ listener2.assert_not_called()
+
+
+class TestEventManager:
+ def test_create_event(self, event_manager: EventManager, listener1: Mock):
+ initial_len = len(event_manager._event_list)
+ event = event_manager.create_event()
+ event.add_listener(listener1)
+ assert len(event_manager._event_list) - initial_len == 1
+ assert isinstance(event_manager._event_list[-1], Event)
+
+ def test_purge_all_event_listeners(self, event_manager: EventManager, listener2: Mock):
+ event_manager.create_event().add_listener(listener2)
+ assert len(event_manager._event_list) == 2
+
+ # Verify events in the manager have listeners associated
+ for event in event_manager._event_list:
+ assert len(event.listeners) == 1
+
+ # Purge listeners and verifying listeners are cleared but events still exist
+ event_manager.purge_all_event_listeners()
+ for event in event_manager._event_list:
+ assert len(event.listeners) == 0
+ assert len(event_manager._event_list) == 2
+
+ def test_purge_all_events(self, event_manager: EventManager, listener2: Mock):
+ """Purges all events in the event list by removing
+ all associated events and listeners
+ """
+ test_event = event_manager.create_event()
+ test_event.add_listener(listener2)
+ assert len(event_manager._event_list) == 2
+
+ # Test purging everything
+ event_manager.purge_all_events()
+ assert len(event_manager._event_list) == 0
+
+ # Test firing a previously linked event
+ test_event.notify()
+ listener2.assert_not_called()
From 95f1b90b4b84d5fc933a017598e0306f5d9ae468 Mon Sep 17 00:00:00 2001
From: Trevor Bayless
Date: Thu, 2 Mar 2023 09:40:10 -0600
Subject: [PATCH 25/25] Temp remove macos-latest testing
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3b509fe..995166b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,7 +42,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
+ os: [ubuntu-latest, windows-latest] # TODO: Test on macos-latest when repo is public
python-version: ["3.7", "3.8", "3.9", "3.10"] # TODO: Add 3.11 after `wrapt` update: https://github.com/GrahamDumpleton/wrapt/issues/226
permissions:
contents: read