diff --git a/.github/workflows/python-linting.yml b/.github/workflows/python-linting.yml index 9623ec7..911ea33 100644 --- a/.github/workflows/python-linting.yml +++ b/.github/workflows/python-linting.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9"] + python-version: ["3.12"] steps: - uses: Actions/checkout@v3 diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 4aec30f..4a691c9 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9","3.10","3.11"] + python-version: ["3.9","3.10","3.11","3.12"] steps: - uses: Actions/checkout@v3 diff --git a/dev_requirements.txt b/dev_requirements.txt index ba9e6ef..5d75287 100644 Binary files a/dev_requirements.txt and b/dev_requirements.txt differ diff --git a/gen_ref_pages.py b/gen_ref_pages.py index ed5958a..0f5fe8c 100644 --- a/gen_ref_pages.py +++ b/gen_ref_pages.py @@ -1,27 +1,27 @@ """Generate the code reference pages.""" +import re from pathlib import Path import mkdocs_gen_files -import re -for path in sorted(Path("src").rglob("*.py")): # - if len(re.findall('(^|/)__',str(path))): +for path in sorted(Path("src").rglob("*.py")): + if len(re.findall("(^|/)__",str(path))): continue - module_path = path.relative_to("src").with_suffix("") # - doc_path = path.relative_to("src").with_suffix(".md") # - full_doc_path = Path("reference", doc_path) # + module_path = path.relative_to("src").with_suffix("") + doc_path = path.relative_to("src").with_suffix(".md") + full_doc_path = Path("reference", doc_path) parts = list(module_path.parts) - if parts[-1] == "__init__": # + if parts[-1] == "__init__": parts = parts[:-1] elif parts[-1] == "__main__": continue - with mkdocs_gen_files.open(full_doc_path, "w") as fd: # - identifier = ".".join(parts) # - print("::: " + identifier, file=fd) # + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + identifier = ".".join(parts) + print("::: " + identifier, file=fd) - mkdocs_gen_files.set_edit_path(full_doc_path, path) # + mkdocs_gen_files.set_edit_path(full_doc_path, path) diff --git a/src/seahorse/execution/__init__.py b/src/seahorse/execution/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/seahorse/execution/exec_multi.py b/src/seahorse/execution/exec_multi.py deleted file mode 100644 index d8a5e9a..0000000 --- a/src/seahorse/execution/exec_multi.py +++ /dev/null @@ -1,140 +0,0 @@ -import asyncio -import csv -from sys import platform - -from split import chop - - -class ExecMulti: - """ - A class to execute multiple rounds and matches of a game. - - Attributes: - main_file (str): The main file to execute. - num_player (int): The number of players in each match. - """ - - def __init__(self, main_file: str, log_file: str = "log.txt") -> None: - """ - Initializes a new instance of the ExecMulti class. - - Args: - main_file (str): The main file to execute. - """ - self.main_file = main_file - self.num_player = 2 - self.log_file = log_file - - async def run_round(self, folder_players: str, name_player1: str, name_player2: str, port: int, recordjs: bool): - """ - Runs a single round of the game. - - Args: - folder_players (str): The folder containing the player files. - name_player1 (str): The name of player 1. - name_player2 (str): The name of player 2. - port (int): The port number for communication. - - Returns: - None - """ - if platform == "win32" : - cmd = "python " + self.main_file + ".py" + " -t local" - if recordjs : - cmd += " --record" - cmd += " -p " + str(port) + " " + folder_players+name_player1 + " " + folder_players+name_player2 - else : - cmd = "python3 " + self.main_file + ".py" + " -t local" - if recordjs : - cmd += " --record" - cmd += " -p " + str(port) + " " + folder_players+name_player1 + " " + folder_players+name_player2 - process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - stdout, stderr = await process.communicate() - with open(self.log_file,"a+") as f : - f.write(stderr.decode(encoding="utf-8")) - f.close() - - async def run_multiple_rounds(self, rounds: int, nb_process: int, swap: bool, folder_players: str, name_player1: str, name_player2: str, port: int = 8080, *, recordjs: bool = False): - """ - Runs multiple rounds of the game. - - Args: - rounds (int): The number of rounds to run. - nb_process (int): The number of processes to use. - swap (bool): Whether to swap the players in alternate rounds. - folder_players (str): The folder containing the player files. - name_player1 (str): The name of player 1. - name_player2 (str): The name of player 2. - port (int): The port number for communication. Default is 8080. - - Returns: - None - """ - for chunk in list(chop(nb_process, range(rounds))): - list_jobs_routines = [] - for add, _ in enumerate(chunk): - if swap: - if add % 2 == 0: - p1 = name_player1 - p2 = name_player2 - else: - p1 = name_player2 - p2 = name_player1 - else: - p1 = name_player1 - p2 = name_player2 - list_jobs_routines.append(asyncio.create_task(self.run_round(folder_players, p1, p2, port+add, recordjs))) - await asyncio.gather(*list_jobs_routines) - - async def run_match(self, rounds_by_match: int, swap: bool, folder_players: str, name_player1: str, name_player2: str, port: int, recordjs: bool): - """ - Runs a single match of the game. - - Args: - rounds_by_match (int): The number of rounds per match. - swap (bool): Whether to swap the players in alternate matches. - folder_players (str): The folder containing the player files. - name_player1 (str): The name of player 1. - name_player2 (str): The name of player 2. - port (int): The port number for communication. - - Returns: - None - """ - await self.run_multiple_rounds(rounds=rounds_by_match, nb_process=1, swap=swap, folder_players=folder_players, name_player1=name_player1, name_player2=name_player2, port=port, recordjs=recordjs) - - async def run_multiple_matches(self, rounds_by_match: int, nb_process: int, swap: bool, folder_players: str, csv_file: str, sep: str = ",", port: int = 8080, *, recordjs: bool = False): - """ - Runs multiple matches of the game. - - Args: - rounds_by_match (int): The number of rounds per match. - nb_process (int): The number of processes to use. - swap (bool): Whether to swap the players in alternate matches. - folder_players (str): The folder containing the player files. - csv_file (str): The CSV file containing the names of the players. - sep (str): The delimiter used in the CSV file. Default is ",". - port (int): The starting port number for communication. Default is 8080. - - Returns: - None - """ - with open(csv_file) as csvfile: - spamreader = csv.reader(csvfile, delimiter=sep) - match_table = [] - for line in spamreader: - counter = 0 - match = [] - for name in line: - match.append(name) - if len(match) == self.num_player: - match_table.append(match) - match = [] - counter = 0 - else: - counter += 1 - for chunk in list(chop(nb_process, match_table)): - list_jobs_routines = [] - for add, match in enumerate(chunk): - list_jobs_routines.append(asyncio.create_task(self.run_match(rounds_by_match, swap, folder_players, match[0], match[1], port+add, recordjs))) - await asyncio.gather(*list_jobs_routines) diff --git a/src/seahorse/game/action.py b/src/seahorse/game/action.py index dcc1643..eb37a58 100644 --- a/src/seahorse/game/action.py +++ b/src/seahorse/game/action.py @@ -1,8 +1,10 @@ from __future__ import annotations from abc import abstractmethod + from seahorse.utils.serializer import Serializable + class Action(Serializable): """ A generic class representing an action in the game. diff --git a/src/seahorse/game/game_state.py b/src/seahorse/game/game_state.py index 74998c1..976913c 100644 --- a/src/seahorse/game/game_state.py +++ b/src/seahorse/game/game_state.py @@ -1,6 +1,7 @@ from abc import abstractmethod from itertools import cycle from typing import Any + from seahorse.game.action import Action from seahorse.game.heavy_action import HeavyAction from seahorse.game.light_action import LightAction @@ -235,5 +236,6 @@ def __eq__(self, value: object) -> bool: def __str__(self) -> str: to_print = f"Current scores are {self.get_scores()}.\n" - to_print += f"Next person to play is player {self.get_next_player().get_id()} ({self.get_next_player().get_name()}).\n" + to_print += f"Next person to play is player {self.get_next_player().get_id()} \ + ({self.get_next_player().get_name()}).\n" return to_print diff --git a/src/seahorse/game/heavy_action.py b/src/seahorse/game/heavy_action.py index 04235bd..a9f894f 100644 --- a/src/seahorse/game/heavy_action.py +++ b/src/seahorse/game/heavy_action.py @@ -62,7 +62,8 @@ def __eq__(self, value: object) -> bool: return hash(self) == hash(value) def __str__(self) -> str: - return "From:\n" + self.get_current_game_state().get_rep().__str__() + "\nto:\n" + self.get_next_game_state().get_rep().__str__() + return "From:\n" + self.get_current_game_state().get_rep().__str__() + "\nto:\n" \ + + self.get_next_game_state().get_rep().__str__() def to_json(self) -> dict: return self.__dict__ diff --git a/src/seahorse/game/io_stream.py b/src/seahorse/game/io_stream.py index ef2e4b4..e8bf286 100644 --- a/src/seahorse/game/io_stream.py +++ b/src/seahorse/game/io_stream.py @@ -3,10 +3,11 @@ import asyncio import functools import json -from collections import deque -import time import re -from typing import TYPE_CHECKING, Any, Callable, Coroutine +import time +from collections import deque +from collections.abc import Coroutine +from typing import TYPE_CHECKING, Any, Callable import socketio from aiohttp import web @@ -33,7 +34,8 @@ def activate(self, Args: identifier (str | None, optional): Must be a unique identifier. Defaults to None. - wrapped_id (int | None, optional): If the eventSlave is bound to an instance a python native id might be associated. Defaults to None. + wrapped_id (int | None, optional): If the eventSlave is instance bounded, a native id might be associated. + Defaults to None. """ self.sio = socketio.AsyncClient() self.connected = False @@ -92,7 +94,8 @@ def remote_action(label: str): def meta_wrapper(fun: Callable): @functools.wraps(fun) async def wrapper(self:EventSlave,current_state:GameState,*_,**kwargs): - await EventMaster.get_instance().sio.emit(label,json.dumps({**current_state.to_json(),**kwargs},default=lambda x:x.to_json()),to=self.sid) + await EventMaster.get_instance().sio.emit(label,json.dumps({**current_state.to_json(),**kwargs}, + default=lambda x:x.to_json()),to=self.sid) out = await EventMaster.get_instance().wait_for_next_play(self.sid,current_state.players) return out @@ -120,7 +123,8 @@ def get_instance(game_state:type=Serializable,port: int = 8080,hostname: str="lo """Gets the instance object Args: - n_clients (int, optional): the number of clients the instance is supposed to be listening, *ignored* if already initialized. Defaults to 1. + n_clients (int, optional): the number of clients the instance is supposed to be listening, + *ignored* if already initialized. Defaults to 1. game_state : class of a game state port (int, optional): the port to use. Defaults to 8080. @@ -133,7 +137,8 @@ def get_instance(game_state:type=Serializable,port: int = 8080,hostname: str="lo def __init__(self,game_state,port,hostname): if EventMaster.__instance is not None: - msg = "Trying to initialize multiple instances of EventMaster, this is forbidden to avoid side-effects.\n Call EventMaster.get_instance() instead." + msg = "Trying to initialize multiple instances of EventMaster, this is forbidden to avoid side-effects.\n\ + Call EventMaster.get_instance() instead." raise NotImplementedError(msg) else: # Initializing attributes @@ -149,7 +154,8 @@ def __init__(self,game_state,port,hostname): self.hostname = hostname # Standard python-socketio server - self.sio = socketio.AsyncServer(async_mode="aiohttp", async_handlers=True, cors_allowed_origins="*", ping_timeout=1e6) + self.sio = socketio.AsyncServer(async_mode="aiohttp", async_handlers=True, + cors_allowed_origins="*", ping_timeout=1e6) self.app = web.Application() # Starting asyncio stuff @@ -176,7 +182,8 @@ def connect(sid, *_): """ self.__open_sessions.add(sid) self.__n_clients_connected += 1 - logger.info(f"Waiting for listeners {self.__n_clients_connected} out of {self.expected_clients} are connected.") + logger.info(f"Waiting for listeners {self.__n_clients_connected} out of " + f"{self.expected_clients} are connected.") @self.sio.event def disconnect(sid): @@ -267,7 +274,9 @@ async def wait_for_event(self,sid:int,label:str,*,flush_until:float | None=None) while not len(self.__events.get(sid,{}).get(label,[])): await asyncio.sleep(.1) ts,data = self.__events[sid][label].pop() - return data if (not flush_until) or ts>=flush_until else await self.wait_for_event(sid,label,flush_until=flush_until) + if (not flush_until) or ts>=flush_until: + return data + await self.wait_for_event(sid,label,flush_until=flush_until) async def wait_for_identified_client(self,name:str,local_id:int) -> str: """ Waits for an identified client (a player typically) @@ -299,10 +308,9 @@ def start(self, task: Callable[[None], None], listeners: list[EventSlave]) -> No Starts an emitting sequence and runs a tasks that embeds calls to `EventMaster.__instance.sio.emit()` - The starting of the game is starting when for all `EventSlave` instances in `listeners`, the `.listen()` future is fulfilled. + The game is starting when for all `EventSlave` in `listeners`, the `.listen()` future is fulfilled. - If `len(listeners)==0` the EventMaster emits events - in the void. + If `len(listeners)==0` the EventMaster emits events in the void. Args: task (Callable[[None],None]): task calling `EventMaster.sio.emit()` @@ -312,20 +320,19 @@ def start(self, task: Callable[[None], None], listeners: list[EventSlave]) -> No # Sets the runner up and starts the tcp server self.event_loop.run_until_complete(self.runner.setup()) - #print(self.port) site = web.TCPSite(self.runner, self.hostname, self.port) self.event_loop.run_until_complete(site.start()) async def stop(): # Waiting for all - logger.info(f"Waiting for listeners {self.__n_clients_connected} out of {self.expected_clients} are connected.") + logger.info(f"Waiting for listeners {self.__n_clients_connected} " + f"out of {self.expected_clients} are connected.") for x in slaves: await x.listen(master_address=f"http://{self.hostname}:{self.port!s}", keep_alive=False) - logger.info("Starting match") - # Launching the task + logger.info("Starting match") task_future = asyncio.wrap_future(self.sio.start_background_task(task)) await task_future diff --git a/src/seahorse/game/light_action.py b/src/seahorse/game/light_action.py index 3b5cc1b..dfc0c33 100644 --- a/src/seahorse/game/light_action.py +++ b/src/seahorse/game/light_action.py @@ -5,6 +5,7 @@ from seahorse.game.action import Action from seahorse.game.heavy_action import HeavyAction from seahorse.utils.custom_exceptions import NoGameStateProvidedError + if TYPE_CHECKING: from seahorse.game.game_state import GameState diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index 09d9eb3..4355898 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -5,7 +5,7 @@ from abc import abstractmethod from collections.abc import Iterable from itertools import cycle -from typing import List, Optional +from typing import Optional from loguru import logger @@ -96,7 +96,8 @@ async def step(self) -> GameState: logger.info(f"time : {self.remaining_time[next_player.get_id()]}") if isinstance(next_player,EventSlave): - action = await next_player.play(self.current_game_state, remaining_time=self.remaining_time[next_player.get_id()]) + action = await next_player.play(self.current_game_state, + remaining_time=self.remaining_time[next_player.get_id()]) else: action = next_player.play(self.current_game_state, remaining_time=self.remaining_time[next_player.get_id()]) tstp = time.time() @@ -108,7 +109,6 @@ async def step(self) -> GameState: if action not in possible_actions: raise ActionNotPermittedError() - # TODO action.current_game_state._possible_actions=None action.current_game_state=None action.next_game_state._possible_actions=None @@ -131,7 +131,8 @@ async def play_game(self) -> list[Player]: logger.info(f"Player : {player.get_name()} - {player.get_id()}") while not self.current_game_state.is_done(): try: - logger.info(f"Player now playing : {self.get_game_state().get_next_player().get_name()} - {self.get_game_state().get_next_player().get_id()}") + logger.info(f"Player now playing : {self.get_game_state().get_next_player().get_name()} - \ + {self.get_game_state().get_next_player().get_id()}") self.current_game_state = await self.step() except (ActionNotPermittedError,SeahorseTimeoutError,StopAndStartError) as e: if isinstance(e,SeahorseTimeoutError): @@ -140,7 +141,8 @@ async def play_game(self) -> list[Player]: logger.error(f"Action not permitted for player {self.current_game_state.get_next_player()}") temp_score = copy.copy(self.current_game_state.get_scores()) id_player_error = self.current_game_state.get_next_player().get_id() - other_player = next(iter([player.get_id() for player in self.current_game_state.get_players() if player.get_id()!=id_player_error])) + other_player = next(iter([player.get_id() for player in self.current_game_state.get_players() if + player.get_id()!=id_player_error])) temp_score[id_player_error] = -1e9 temp_score[other_player] = 1e9 self.winner = self.compute_winner(temp_score) @@ -176,7 +178,7 @@ async def play_game(self) -> list[Player]: logger.verdict(f"{','.join(w.get_name() for w in self.get_winner())} has won the game") return self.winner - def record_game(self, listeners:Optional[List[EventSlave]]=None) -> None: + def record_game(self, listeners:Optional[list[EventSlave]]=None) -> None: """ Starts a game and broadcasts its successive states. """ diff --git a/src/seahorse/game/time_manager.py b/src/seahorse/game/time_manager.py deleted file mode 100644 index b624854..0000000 --- a/src/seahorse/game/time_manager.py +++ /dev/null @@ -1,265 +0,0 @@ -# import builtins -# import functools -# import time -# from typing import Any - -# from seahorse.utils.custom_exceptions import ( -# AlreadyRunningError, -# NotRunningError, -# SeahorseTimeoutError, -# TimerNotInitializedError, -# ) - - -# class TimeMaster: -# __instance = None - -# class Timer: -# def __init__(self,time_limit:float=1e9): -# self._time_limit = time_limit -# self._remaining_time = time_limit -# self._last_timestamp = None -# self._is_running = False - -# def start_timer(self) -> float: -# """Starts the timer - -# Raises: -# AlreadyRunningException: when trying to start twice. -# """ -# if self._is_running: -# raise AlreadyRunningError() - -# self._last_timestamp = time.time() - -# self._is_running = True - -# return self._last_timestamp - -# def is_running(self) -> bool: -# """ -# Is the timer running ? - -# Returns: -# bool: `True` if the timer is running, `False` otherwise -# """ -# return self._is_running - -# def get_time_limit(self): -# """ -# Get the limit set in `set_time_limit()` -# """ -# return self._time_limit - -# def get_last_timestamp(self): -# """ -# Get the last timestamp set at start_timer() -# """ -# return self._last_timestamp - -# def get_remaining_time(self) -> float: -# """Gets the timer's remaining time - -# Returns: -# float: the remaining time -# """ -# if self._is_running: -# return self._remaining_time - (time.time() - self._last_timestamp) -# else: -# return self._remaining_time - -# def stop_timer(self) -> float: -# """Pauses the timer - -# Raises: -# NotRunningException: when the timer isn't running - -# Returns: -# float: remaining time -# """ -# if not self._is_running: -# raise NotRunningError() - -# self._remaining_time = self._remaining_time - (time.time() - self._last_timestamp) - -# self._is_running = False -# return self._remaining_time - - -# def is_locked(self) -> bool: -# """Is the time credit expired ? - -# Returns: -# bool: `True` if expired `False` otherwise -# """ -# #logger.info(f"time : {self.get_remaining_time()}") -# return self.get_remaining_time() <= 0 - -# @staticmethod -# def get_instance()->"TimeMaster": -# if TimeMaster.__instance is None: -# TimeMaster.__instance=TimeMaster() -# return TimeMaster.__instance - -# @classmethod -# def register_timer(cls: "TimeMaster", linked_instance: Any, time_limit:float=1e9): -# pid = linked_instance.__dict__.get("id",builtins.id(linked_instance)) -# cls.get_instance().__time_register[pid]=cls.get_instance().__time_register.get(pid,TimeMaster.Timer(time_limit)) - -# @classmethod -# def get_timer(cls: "TimeMaster", linked_instance: Any)-> Timer: -# return cls.get_instance().__time_register.get(linked_instance.__dict__.get("id",builtins.id(linked_instance))) - - -# def __init__(self): -# if TimeMaster.__instance is not None: -# msg = "Trying to initialize multiple instances of TimeMaster, this is forbidden to avoid side-effects.\n Call TimeMaster.get_instance() instead." -# raise NotImplementedError(msg) -# else: -# self.__time_register={} - -# class TimeMixin: -# """ -# When implemented allows any object to keep track of time - -# Example usage: -# ``` -# import time -# class MyTimedObject(TimeMixin): -# def __init__(self): -# self.myattr = 2 - -# x = MyTimedObject() -# x.set_time_limit(10) -# x.start_timer() -# time.sleep(11) -# x.myattr=5 # raises SeahorseTimeoutException - -# ``` -# """ - -# def init_timer(self, time_limit: int) -> None: -# """ -# Initializes the time credit of the instance - -# Doesn't start the timer yet ! Call `start_timer()`. - -# Args: -# time_limit (int): max time before locking all methods of the class -# """ -# TimeMaster.register_timer(self,time_limit) - -# def start_timer(self) -> float: -# """Starts the timer - -# Raises: -# AlreadyRunningException: when trying to start twice. -# """ -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# return TimeMaster.get_timer(self).start_timer() - -# def is_running(self) -> bool: -# """ -# Is the timer running ? - -# Returns: -# bool: `True` if the timer is running, `False` otherwise -# """ -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# return TimeMaster.get_timer(self).is_running() - -# def get_time_limit(self): -# """ -# Get the limit set in `set_time_limit()` -# """ -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# return TimeMaster.get_timer(self).get_time_limit() - -# def get_remaining_time(self) -> float: -# """Gets the timer's remaining time - -# Returns: -# float: the remaining time -# """ -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# return TimeMaster.get_timer(self).get_remaining_time() - -# def get_last_timestamp(self) -> float: -# """Gets the timer's last recorded timestamp at which it was started - -# Returns: -# float: the timestamp -# """ -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# return TimeMaster.get_timer(self).get_last_timestamp() - -# def stop_timer(self) -> float: -# """Pauses the timer - -# Raises: -# NotRunningException: when the timer isn't running - -# Returns: -# float: remaining time -# """ -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# return TimeMaster.get_timer(self).stop_timer() - - -# def is_locked(self) -> bool: -# """Is the time credit expired ? - -# Returns: -# bool: `True` if expired `False` otherwise -# """ -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# return TimeMaster.get_timer(self).is_locked() - -# def __setattr__(self, __name: str, value: Any) -> None: -# """_summary_ - -# Args: -# Inherited from object -# Raises: -# TimeoutException: prevents modification after timout - -# """ -# try: -# if TimeMaster.get_timer(self) and self.is_locked(): -# raise SeahorseTimeoutError() -# else: -# self.__dict__[__name] = value -# except Exception as e: -# raise e - -# def timed_function(fun): -# """ -# Decorator to prevent using a function after object's timeout. -# Args: -# fun (_type_): wrapped function - -# Raises: -# TimerNotInitializedError: _description_ -# Exception: _description_ -# SeahorseTimeoutError: _description_ - - -# Returns: -# Callable[...]: wrapper -# """ -# @functools.wraps(fun) -# def wrapper(self, *args, **kwargs): -# r = fun(self, *args, **kwargs) -# if TimeMaster.get_timer(self) is None: -# raise TimerNotInitializedError -# elif(self.is_locked()): -# raise SeahorseTimeoutError() -# return r -# return wrapper diff --git a/src/seahorse/player/player.py b/src/seahorse/player/player.py index 9daa569..78fa9b0 100644 --- a/src/seahorse/player/player.py +++ b/src/seahorse/player/player.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from seahorse.game.action import Action -# from seahorse.game.time_manager import TimeMixin, timed_function from seahorse.utils.custom_exceptions import MethodNotImplementedError from seahorse.utils.serializer import Serializable @@ -22,7 +21,7 @@ class Player(Serializable): name (str) : the name of the player """ - def __init__(self, name: str = "bob",*,id:int | None = None,**_) -> None: + def __init__(self, name: str = "bob", *, identifier:int | None = None,**_) -> None: """ Initializes a new instance of the Player class. @@ -31,10 +30,10 @@ def __init__(self, name: str = "bob",*,id:int | None = None,**_) -> None: hard_id (int, optional, keyword-only): Set the player's id in case of distant loading """ self.name = name - if id is None: - self.id = builtins.id(self) + if identifier is None: + self.identifier = builtins.id(self) else: - self.id = id + self.identifier = identifier def play(self, current_state: GameState, remaining_time: int) -> Action: @@ -50,7 +49,6 @@ def play(self, current_state: GameState, remaining_time: int) -> Action: Returns: Action: The resulting action. """ - # TODO: check score ???? return self.compute_action(current_state=current_state, remaining_time=remaining_time) @abstractmethod @@ -74,7 +72,7 @@ def get_id(self) -> int: Returns: int: The ID of the player. """ - return self.id + return self.identifier def get_name(self) -> str: """ diff --git a/src/seahorse/player/proxies.py b/src/seahorse/player/proxies.py index 134afc3..a91429c 100644 --- a/src/seahorse/player/proxies.py +++ b/src/seahorse/player/proxies.py @@ -1,11 +1,11 @@ import json -from collections.abc import Coroutine import time +from collections.abc import Coroutine from typing import Any, Optional from loguru import logger -from seahorse.game.action import Action +from seahorse.game.action import Action from seahorse.game.game_state import GameState from seahorse.game.io_stream import EventMaster, EventSlave, event_emitting, remote_action from seahorse.game.light_action import LightAction diff --git a/src/seahorse/tournament/__init__.py b/src/seahorse/tournament/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/seahorse/tournament/challonge_tournament.py b/src/seahorse/tournament/challonge_tournament.py deleted file mode 100644 index 2a8a7ab..0000000 --- a/src/seahorse/tournament/challonge_tournament.py +++ /dev/null @@ -1,269 +0,0 @@ -from __future__ import annotations - -import asyncio -import csv -import math -from sys import platform - -import challonge -from split import chop - -from seahorse.utils.custom_exceptions import ConnectionProblemError, NoTournamentFailError - - -class ChallongeTournament: - """ - A class to interact with the Challonge tournament platform. - - Attributes: - id_challonge (str): The Challonge ID. - keypass_challonge (str): The Challonge API key. - game_name (str): The name of the game. - log_level (str): The log file. - """ - - def __init__(self, id_challonge: str, keypass_challonge: str, game_name: str, log_level: str|None, log_file: str = "log.txt") -> None: - """ - Initializes a new instance of the ChallongeTournament class. - - Args: - id_challonge (str): The Challonge ID. - keypass_challonge (str): The Challonge API key. - game_name (str): The name of the game. - log_level (str): The log file. Default is None. - """ - self.id_challonge = id_challonge - self.keypass_challonge = keypass_challonge - self.game_name = game_name - self.log_level = log_level - self.user = None - self.tournament = None - self.log_file = log_file - self.created = False - - async def create_tournament(self, tournament_name: str, tournament_url: str, csv_file: str, sep: str = ",") -> None: - """ - Creates a new tournament on Challonge and adds participants from a CSV file. - - Args: - tournament_name (str): The name of the tournament. - tournament_url (str): The URL of the tournament. - csv_file (str): The path to the CSV file containing participant names. - sep (str): The delimiter used in the CSV file. Default is ",". - - Returns: - None - """ - self.user = await challonge.get_user(self.id_challonge, self.keypass_challonge) - self.tournament = await self.user.create_tournament(name=tournament_name, url=tournament_url) - with open(csv_file) as csvfile : - spamreader = csv.reader(csvfile, delimiter=sep) - for line in spamreader : - for name in line : - await self.tournament.add_participant(str(name)) - self.created = True - - async def connect_tournament(self, tournament_name: str) -> None: - """ - Connects to an existing tournament on Challonge. - - Args: - tournament_name (str): The name of the tournament. - - Returns: - None - - Raises: - ConnectionProblemError: If the connection to the tournament fails. - """ - self.user = await challonge.get_user(self.id_challonge, self.keypass_challonge) - my_tournaments = await self.user.get_tournaments() - for t in my_tournaments: - if t.name == tournament_name : - self.tournament = t - return - raise ConnectionProblemError() - - def retrieve_scores(self, match) -> str: - """ - Retrieves the scores from a match. - - Args: - match: The match object. - - Returns: - str: The scores as a string. - """ - if not match.scores_csv : - return match.scores_csv - return match.scores_csv + "," - - def retrieve_winners(self, scores: str, p1, p2, minormax: str) -> list : - """ - Retrieves the winners from the scores. - - Args: - scores (str): The scores as a string. - p1: The participant object of player 1. - p2: The participant object of player 2. - - Returns: - list: A list of winners. - """ - result = [] - if not scores : - return result - list_scores = scores[:-1].split(",") - for score in list_scores: - s1, s2 = score.split("-") - if minormax == "max" : - if s1 > s2 : - result.append(p1) - elif s1 < s2 : - result.append(p2) - elif minormax == "min" : - if s1 > s2 : - result.append(p2) - elif s1 < s2 : - result.append(p1) - return result - - def invert_score(self, score: str) -> str: - """ - Inverts the score. - - Args: - score (str): The score as a string. - - Returns: - str: The inverted score. - """ - list_score = score[:-1].split("-") - return list_score[1] + "-" + list_score[0] + "," - - def get_participant_winner(self, winner: str, p1, p2): - """ - Gets the participant object of the winner. - - Args: - winner (str): The name of the winner. - p1: The participant object of player 1. - p2: The participant object of player 2. - - Returns: - The participant object of the winner. - """ - if winner == p1.name : - return p1 - else : - return p2 - - async def play_round(self,name1: str, name2: str, port: int, folder_player: str) -> tuple[str, str]: - """ - Plays a round of the tournament. - - Args: - name1 (str): The name of player 1. - name2 (str): The name of player 2. - port (int): The port number. - folder_player (str): The folder containing the player scripts. - - Returns: - tuple[str, str]: A tuple containing the score and the winner. - """ - if platform == "win32" : - cmd = "python " + self.game_name + ".py" + " -t local -p " + str(port) + " " + folder_player+name1 + " " + folder_player+name2 - else : - cmd = "python3 " + self.game_name + ".py" + " -t local -p " + str(port) + " " + name1 + " " + name2 - process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - stdout, stderr = await process.communicate() - score1 = stderr.decode("utf-8").split("\n")[-4].split(" - ")[-1] - score2 = stderr.decode("utf-8").split("\n")[-3].split(" - ")[-1] - winner = stderr.decode("utf-8").split("\n")[-2].split(" - ")[-1] - score = str(math.floor(abs(float(score1)))) + "-" + str(math.floor(abs(float(score2)))) + "," - winner = str(winner) - with open(self.log_file,"a+") as f : - f.write(stderr.decode(encoding="utf-8")) - f.close() - return score, winner - - async def play_match(self, match, port: int, rounds: int, folder_player: str, minormax: str) -> None: - """ - Plays a match of the tournament. - - Args: - match: The match object. - port (int): The port number. - rounds (int): The number of rounds. - folder_player (str): The folder containing the player scripts. - - Returns: - None - """ - if match.completed_at is None : - p1 = await self.tournament.get_participant(match.player1_id) - p2 = await self.tournament.get_participant(match.player2_id) - already_played = 0 - if match.underway_at is None : - await match.mark_as_underway() - scores = "" - winners = [] - else : - scores = self.retrieve_scores(match) - winners = self.retrieve_winners(scores, p1, p2, minormax) - already_played = len(winners) - for r in range(already_played, rounds) : - if r % 2 == 0 : - score, winner = await self.play_round(p1.name, p2.name, port, folder_player) - scores += score - winners.append(self.get_participant_winner(winner, p1, p2)) - else : - score, winner = await self.play_round(p2.name, p1.name, port, folder_player) - scores += self.invert_score(score) - winners.append(self.get_participant_winner(winner, p1, p2)) - await match.report_live_scores(scores[:-1]) - if match.group_id is not None : - await match._report(scores[:-1], max(winners,key=winners.count).group_player_ids[0]) - else : - await match._report(scores[:-1], max(winners,key=winners.count).id) - await match.unmark_as_underway() - - async def run(self, folder_player: str, rounds: int = 1, nb_process: int = 2, minormax: str = "max") -> None: - """ - Runs the tournament. - - Args: - folder_player (str): The folder containing the player scripts. - rounds (int): The number of rounds. Default is 1. - nb_process (int): The number of parallel processes. Default is 2. - - Returns: - None - - Raises: - NoTournamentFailError: If there is no tournament. - """ - if self.tournament is not None : - if self.created : - await self.tournament.start() - matches = await self.tournament.get_matches() - dict_round = {} - for match in matches : - if match.group_id is None : - if dict_round.get(match.round,False) : - dict_round[match.round] += [match] - else : - dict_round[match.round] = [match] - elif dict_round.get(match.group_id,False) : - dict_round[match.group_id] += [match] - else : - dict_round[match.group_id] = [match] - for key in dict_round.keys() : - port = 16000 - for matches in list(chop(nb_process, dict_round[key])) : - list_jobs_routines = [asyncio.create_task(self.play_match(match, port+i, rounds, folder_player, minormax)) for i, match in enumerate(matches)] - await asyncio.gather(*list_jobs_routines) - if self.created : - await self.tournament.finalize() - else : - raise NoTournamentFailError() diff --git a/src/seahorse/utils/custom_exceptions.py b/src/seahorse/utils/custom_exceptions.py index 1b87d63..996ee8a 100644 --- a/src/seahorse/utils/custom_exceptions.py +++ b/src/seahorse/utils/custom_exceptions.py @@ -86,6 +86,7 @@ class NoTournamentFailError(Exception): """Thrown when trying to generate an action that's not permitted """ - def __init__(self, message: str = "Tournament problem : tournament is none, please connect to an existing tournament or create a tournament"): + def __init__(self, message: str = "Tournament problem : tournament is none, please connect \ + to an existing tournament or create a tournament"): self.message = message super().__init__(message) diff --git a/src/seahorse/utils/gui_client.py b/src/seahorse/utils/gui_client.py index 5b26823..8bdf313 100644 --- a/src/seahorse/utils/gui_client.py +++ b/src/seahorse/utils/gui_client.py @@ -1,12 +1,15 @@ import builtins import os -import subprocess import platform -from typing import Any, Coroutine, Optional +import subprocess +from collections.abc import Coroutine +from typing import Any, Optional from loguru import logger + from seahorse.game.io_stream import EventMaster, EventSlave + class GUIClient(EventSlave): def __init__(self, path:Optional[str]=None) -> None: self.id = builtins.id(self) @@ -21,11 +24,12 @@ def open_file(url): except AttributeError: try: if platform.system() == "Linux": - subprocess.call(["xdg-open", url]) + subprocess.check_call(["xdg-open", url]) elif platform.system() == "Darwin": - subprocess.call(["open", url]) + subprocess.check_call(["open", url]) else: - raise Exception("Unexpected platform") + msg = "Unexpected platform" + raise Exception(msg) except Exception: logger.debug("Could not open URL") diff --git a/tests/test_base.py b/tests/test_base.py index 8722528..0733fb1 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,18 +1,17 @@ -from typing import Any, Dict, List -import unittest import copy -from random import sample +import unittest +from typing import Any -from seahorse.game.game_layout.board import Piece, Board -from seahorse.game.representation import Representation -from seahorse.player.player import Player +from seahorse.game.game_layout.board import Board, Piece from seahorse.game.game_state import GameState from seahorse.game.heavy_action import HeavyAction +from seahorse.game.representation import Representation +from seahorse.player.player import Player -class Dummy_GameState(GameState): +class DummyGameState(GameState): - def __init__(self, scores: Dict[int, Any], next_player: Player, players: List[Player], rep: Representation) -> None: + def __init__(self, scores: dict[int, Any], next_player: Player, players: list[Player], rep: Representation) -> None: super().__init__(scores, next_player, players, rep) def generate_possible_actions(self): @@ -28,7 +27,7 @@ def generate_possible_actions(self): poss_actions = { HeavyAction( self, - Dummy_GameState( + DummyGameState( self.get_scores(), self.compute_next_player(), self.players, @@ -41,19 +40,20 @@ def generate_possible_actions(self): return poss_actions -class test_case(unittest.TestCase): +class TestCase(unittest.TestCase): def setUp(self): self.board = Board(env={}, dim=[3, 3]) self.player1 = Player("Thomas") - self.player2 = Player(id=42) + self.player2 = Player(identifier=42) self.piece1 = Piece("A") self.piece2 = Piece("B", self.player2) self.piece3 = Piece("C", self.player1) - self.current_gs = Dummy_GameState(scores={self.player1.get_id():1, self.player2.get_id():0}, next_player=self.player1, players=[self.player1, self.player2], rep=self.board) + self.current_gs = DummyGameState(scores={self.player1.get_id():1, self.player2.get_id():0}, + next_player=self.player1, players=[self.player1, self.player2], rep=self.board) def test_id(self): assert self.player1.get_id() == id(self.player1) @@ -76,8 +76,10 @@ def test_gamestate(self): assert self.current_gs.get_player_score(self.player1) == 1 self.board.env[(0, 1)] = self.piece1 possible_actions = self.current_gs.generate_possible_actions() - assert len(possible_actions) == self.current_gs.get_rep().get_dimensions()[0]*self.current_gs.get_rep().get_dimensions()[1] - 1 + assert len(possible_actions) == self.current_gs.get_rep().get_dimensions()[0]*self.current_gs.get_rep()\ + .get_dimensions()[1] - 1 self.board.env[(2, 1)] = self.piece2 self.board.env[(2, 2)] = self.piece3 possible_actions = self.current_gs.generate_possible_actions() - assert len(possible_actions) == self.current_gs.get_rep().get_dimensions()[0]*self.current_gs.get_rep().get_dimensions()[1] - 3 + assert len(possible_actions) == self.current_gs.get_rep().get_dimensions()[0]*self.current_gs.get_rep()\ + .get_dimensions()[1] - 3