From b88683a537a471f53709c231cd69856f7b9a76ac Mon Sep 17 00:00:00 2001 From: alexkar598 <25136265+alexkar598@users.noreply.github.com> Date: Sun, 10 Mar 2024 21:38:45 -0400 Subject: [PATCH 1/6] Update gui_client.py --- src/seahorse/utils/gui_client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/seahorse/utils/gui_client.py b/src/seahorse/utils/gui_client.py index 9cc0547..e0767cb 100644 --- a/src/seahorse/utils/gui_client.py +++ b/src/seahorse/utils/gui_client.py @@ -1,6 +1,7 @@ import builtins import os import subprocess +import platform from typing import Any, Coroutine, Optional from loguru import logger @@ -19,7 +20,12 @@ def open_file(url): os.startfile(url) except AttributeError: try: - subprocess.call(["open", url]) + if platform.system() == "Linux": + subprocess.call(["xdg-open", url]) + elif platform.system() == "Darwin": + subprocess.call(["open", url]) + else: + raise Exception("Unexpected platform") except Exception: logger.debug("Could not open URL") From b918a0fb4ac93e1e9bc8c2d6a9a6239db30adf32 Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Tue, 21 May 2024 17:33:04 -0400 Subject: [PATCH 2/6] remove time manager --- src/seahorse/game/master.py | 75 +++-- src/seahorse/game/time_manager.py | 528 +++++++++++++++--------------- src/seahorse/player/player.py | 14 +- src/seahorse/player/proxies.py | 10 +- 4 files changed, 320 insertions(+), 307 deletions(-) diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index 42a4d96..d69d5b9 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -41,7 +41,8 @@ def __init__( players_iterator: Iterable[Player], log_level: str = "INFO", port: int =8080, - hostname: str ="localhost" + hostname: str ="localhost", + time_limit: int = 15*60, ) -> None: """ Initializes a new instance of the GameMaster class. @@ -54,10 +55,12 @@ def __init__( log_level (str): The name of the log file. """ self.timetol = 1e-1 - self.recorded_plays = [] + # self.recorded_plays = [] self.name = name self.current_game_state = initial_game_state self.players = initial_game_state.players + self.remaining_time = {player.get_id(): time_limit for player in self.players} + player_names = [x.name for x in self.players] if len(set(player_names)) GameState: possible_actions = self.current_game_state.get_possible_actions() start = time.time() - next_player.start_timer() - logger.info(f"time : {next_player.get_remaining_time()}") + # next_player.start_timer() + 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) + 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) - + action = next_player.play(self.current_game_state, remaining_time=self.remaining_time[next_player.get_id()]) tstp = time.time() - if abs((tstp-start)-(tstp-next_player.get_last_timestamp()))>self.timetol: - next_player.stop_timer() - raise StopAndStartError() + self.remaining_time[next_player.get_id()] -= (tstp-start) + if self.remaining_time[next_player.get_id()] < 0: + raise SeahorseTimeoutError() + + # if abs((tstp-start)-(tstp-next_player.get_last_timestamp()))>self.timetol: + # next_player.stop_timer() + # raise StopAndStartError() - next_player.stop_timer() + # next_player.stop_timer() if action not in possible_actions: raise ActionNotPermittedError() @@ -124,9 +130,9 @@ async def play_game(self) -> list[Player]: "play", json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()), ) - self.recorded_plays.append(self.current_game_state.__class__.from_json(json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()))) + # self.recorded_plays.append(self.current_game_state.__class__.from_json(json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()))) id2player={} - verdict_scores=[-1e9,-1e9] + # verdict_scores=[-1e9,-1e9] for player in self.get_game_state().get_players() : id2player[player.get_id()]=player.get_name() logger.info(f"Player : {player.get_name()} - {player.get_id()}") @@ -134,33 +140,40 @@ async def play_game(self) -> list[Player]: 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()}") self.current_game_state = await self.step() - self.recorded_plays.append(self.current_game_state.__class__.from_json(json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()))) + # self.recorded_plays.append(self.current_game_state.__class__.from_json(json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()))) except (ActionNotPermittedError,SeahorseTimeoutError,StopAndStartError) as e: if isinstance(e,SeahorseTimeoutError): logger.error(f"Time credit expired for player {self.current_game_state.get_next_player()}") elif isinstance(e,ActionNotPermittedError) : logger.error(f"Action not permitted for player {self.current_game_state.get_next_player()}") - else: - logger.error(f"Player {self.current_game_state.get_next_player()} might have tried tampering with the timer.\n The timedelta difference exceeded the allowed tolerancy in GameMaster.timetol ") + # else: + # logger.error(f"Player {self.current_game_state.get_next_player()} might have tried tampering with the timer.\n The timedelta difference exceeded the allowed tolerancy in GameMaster.timetol ") temp_score = copy.copy(self.current_game_state.get_scores()) id_player_error = self.current_game_state.get_next_player().get_id() - temp_score.pop(id_player_error) - self.winner = self.compute_winner(temp_score) - self.current_game_state.get_scores()[id_player_error] = -3 other_player = next(iter([player.get_id() for player in self.current_game_state.get_players() if player.get_id()!=id_player_error])) - self.current_game_state.get_scores()[other_player] = 0 - scores = self.get_scores() - for key in scores.keys(): - verdict_scores[int(id2player[key].split("_")[-1])-1]=-scores[key] - logger.info(f"{id2player[key]}:{scores[key]}") + temp_score[id_player_error] = -1e9 + temp_score[other_player] = 1e9 + + # temp_score.pop(id_player_error) + self.winner = self.compute_winner(temp_score) + + # self.current_game_state.get_scores()[id_player_error] = -3 + # other_player = next(iter([player.get_id() for player in self.current_game_state.get_players() if player.get_id()!=id_player_error])) + # self.current_game_state.get_scores()[other_player] = 0 + + # scores = self.get_scores() + for key in temp_score.keys(): + # verdict_scores[int(id2player[key].split("_")[-1])-1]=-scores[key] + logger.info(f"{id2player[key]}:{temp_score[key]}") + for player in self.get_winner() : logger.info(f"Winner - {player.get_name()}") await self.emitter.sio.emit("done",json.dumps(self.get_scores())) - logger.verdict(f"{verdict_scores[::-1]}") - with open(self.players[0].name+"_"+self.players[-1].name+"_"+str(time.time())+".json","w+") as f: - f.write(json.dumps(self.recorded_plays),default=lambda x:x.to_json()) + + logger.verdict(f"{self.current_game_state.get_next_player().get_name()} has been disqualified") + return self.winner logger.info(f"Current game state: \n{self.current_game_state.get_rep()}") @@ -173,15 +186,15 @@ async def play_game(self) -> list[Player]: self.winner = self.compute_winner(self.current_game_state.get_scores()) scores = self.get_scores() for key in scores.keys() : - verdict_scores[int(id2player[key].split("_")[-1])-1]=-scores[key] + # verdict_scores[int(id2player[key].split("_")[-1])-1]=-scores[key] logger.info(f"{id2player[key]}:{(scores[key])}") + for player in self.get_winner() : logger.info(f"Winner - {player.get_name()}") await self.emitter.sio.emit("done",json.dumps(self.get_scores())) - logger.verdict(f"{verdict_scores[::-1]}") - with open(self.players[0].name+"_"+self.players[-1].name+"_"+str(time.time())+".json","w+") as f: - f.write(json.dumps(self.recorded_plays,default=lambda x:x.to_json())) + 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: diff --git a/src/seahorse/game/time_manager.py b/src/seahorse/game/time_manager.py index a82a4b2..b624854 100644 --- a/src/seahorse/game/time_manager.py +++ b/src/seahorse/game/time_manager.py @@ -1,265 +1,265 @@ -import builtins -import functools -import time -from typing import Any - -from seahorse.utils.custom_exceptions import ( - AlreadyRunningError, - NotRunningError, - SeahorseTimeoutError, - TimerNotInitializedError, -) - +# 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 +# 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 24284c6..bfe4e30 100644 --- a/src/seahorse/player/player.py +++ b/src/seahorse/player/player.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from seahorse.game.action import Action -from seahorse.game.time_manager import TimeMixin, timed_function +# from seahorse.game.time_manager import TimeMixin, timed_function from seahorse.utils.custom_exceptions import MethodNotImplementedError from seahorse.utils.serializer import Serializable @@ -13,7 +13,7 @@ from seahorse.game.game_state import GameState -class Player(Serializable,TimeMixin): +class Player(Serializable):#,TimeMixin): """ A base class representing a player in the game. @@ -22,7 +22,7 @@ class Player(Serializable,TimeMixin): name (str) : the name of the player """ - def __init__(self, name: str = "bob", time_limit: float = 1e6,*,id:int | None = None,**_) -> None: + def __init__(self, name: str = "bob",*,id:int | None = None,**_) -> None: """ Initializes a new instance of the Player class. @@ -36,11 +36,11 @@ def __init__(self, name: str = "bob", time_limit: float = 1e6,*,id:int | None = self.id = builtins.id(self) else: self.id = id - self.init_timer(time_limit) + # self.init_timer(time_limit) - @timed_function - def play(self, current_state: GameState) -> Action: + # @timed_function + def play(self, current_state: GameState, remaining_time: int) -> Action: """ Implements the player's logic and calls compute_action with minimal information. @@ -54,7 +54,7 @@ def play(self, current_state: GameState) -> Action: Action: The resulting action. """ # TODO: check score ???? - return self.compute_action(current_state=current_state) + return self.compute_action(current_state=current_state, remaining_time=remaining_time) @abstractmethod def compute_action(self, **kwargs) -> Action: diff --git a/src/seahorse/player/proxies.py b/src/seahorse/player/proxies.py index 6343703..70d4ff9 100644 --- a/src/seahorse/player/proxies.py +++ b/src/seahorse/player/proxies.py @@ -38,7 +38,7 @@ def __init__(self, mimics: type[Player], *args, **kwargs) -> None: self.sid = None @remote_action("turn") - def play(self, *,current_state: GameState) -> Action: + def play(self, *,current_state: GameState, remaining_time: int) -> Action: """ Plays a move. @@ -107,7 +107,7 @@ async def update_id(data): self.wrapped_player.id = json.loads(data)["new_id"] @event_emitting("action") - def play(self, current_state: GameState) -> Action: + def play(self, current_state: GameState, remaining_time: int) -> Action: """ Plays a move. @@ -117,7 +117,7 @@ def play(self, current_state: GameState) -> Action: Returns: Action: The action resulting from the move. """ - return self.compute_action(current_state=current_state) + return self.compute_action(current_state=current_state, remaining_time=remaining_time) def __getattr__(self, attr): return getattr(self.wrapped_player, attr) @@ -129,7 +129,7 @@ def __eq__(self, __value: object) -> bool: return hash(self) == hash(__value) def __str__(self) -> str: - return f"Player {self.wrapped_player.get_name()} has ID {self.wrapped_player.get_id()}." + return f"Player {self.wrapped_player.get_name()} (ID: {self.wrapped_player.get_id()})." def to_json(self) -> dict: return self.wrapped_player.to_json() @@ -151,7 +151,7 @@ def __init__(self, mimics: Player, gui_path:Optional[str]=None, *args, **kwargs) self.shared_sid = None self.sid = None - async def play(self, current_state: GameState) -> Action: + async def play(self, current_state: GameState, **args) -> Action: if self.shared_sid and not self.sid: self.sid=self.shared_sid.sid while True: From ec38f53e3e2fb437a9b167d2c5b0ff67756155b7 Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Fri, 24 May 2024 14:01:37 -0400 Subject: [PATCH 3/6] [FIX] Timer when its used remotly --- src/seahorse/game/action.py | 47 ++++----------------- src/seahorse/game/game_state.py | 68 +++++++++++++++++++++++++----- src/seahorse/game/heavy_action.py | 69 +++++++++++++++++++++++++++++++ src/seahorse/game/light_action.py | 53 ++++++++++++++++++++++++ src/seahorse/game/master.py | 7 +++- src/seahorse/player/proxies.py | 7 +++- 6 files changed, 199 insertions(+), 52 deletions(-) create mode 100644 src/seahorse/game/heavy_action.py create mode 100644 src/seahorse/game/light_action.py diff --git a/src/seahorse/game/action.py b/src/seahorse/game/action.py index 9f1cc09..f78617f 100644 --- a/src/seahorse/game/action.py +++ b/src/seahorse/game/action.py @@ -1,59 +1,30 @@ from __future__ import annotations +from abc import abstractmethod from typing import TYPE_CHECKING from seahorse.utils.serializer import Serializable -if TYPE_CHECKING: - from seahorse.game.game_state import GameState - - class Action(Serializable): """ - A class representing an action in the game. + A generic class representing an action in the game. - Attributes: - past_gs (GameState): The past game state. - new_gs (GameState): The new game state. """ - def __init__(self, current_game_state: GameState, next_game_state: GameState) -> None: + def __init__(self) -> None: """ Initializes a new instance of the Action class. - Args: - past_gs (GameState): The past game state. - new_gs (GameState): The new game state. - """ - self.current_game_state = current_game_state - self.next_game_state = next_game_state - - def get_current_game_state(self) -> GameState: - """ - Returns the past game state. - - Returns: - GameState: The past game state. """ - return self.current_game_state + pass - def get_next_game_state(self) -> GameState: + @abstractmethod + def get_heavy_action(self, **kwargs) -> Action: """ - Returns the new game state. + Returns the heavy action. Returns: - GameState: The new game state. + Action: The heavy action. """ - return self.next_game_state - - def __hash__(self) -> int: - return hash((hash(self.get_next_game_state()), hash(self.get_current_game_state()))) - - 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__() + raise NotImplementedError - def to_json(self) -> dict: - return self.__dict__ diff --git a/src/seahorse/game/game_state.py b/src/seahorse/game/game_state.py index f1ec1e0..6acf73f 100644 --- a/src/seahorse/game/game_state.py +++ b/src/seahorse/game/game_state.py @@ -3,6 +3,8 @@ from typing import Any from seahorse.game.action import Action +from seahorse.game.heavy_action import HeavyAction +from seahorse.game.light_action import LightAction from seahorse.game.representation import Representation from seahorse.player.player import Player from seahorse.utils.custom_exceptions import MethodNotImplementedError @@ -34,7 +36,8 @@ def __init__(self, scores: dict[int, Any], next_player: Player, players: list[Pl self.next_player = next_player self.players = players self.rep = rep - self._possible_actions = None + self._possible_light_actions = None + self._possible_heavy_actions = None def get_player_score(self, player: Player) -> float: """ @@ -95,10 +98,25 @@ def get_rep(self) -> Representation: """ return self.rep - def get_possible_actions(self) -> frozenset[Action]: + def get_possible_light_actions(self) -> frozenset[LightAction]: """ - Returns a copy of the possible actions from this state. - The first call triggers the `generate_possible_actions` method. + Returns a copy of the possible light actions from this state. + The first call triggers the `generate_possible_light_actions` method. + + Returns: + FrozenSet[LightAction]: The possible actions. + """ + # Lazy loading + if self.is_done(): + return frozenset() + if self._possible_light_actions is None: + self._possible_light_actions = frozenset(self.generate_possible_light_actions()) + return self._possible_light_actions + + def get_possible_heavy_actions(self) -> frozenset[HeavyAction]: + """ + Returns a copy of the possible heavy actions from this state. + The first call triggers the `generate_possible_heavy_actions` method. Returns: FrozenSet[Action]: The possible actions. @@ -106,9 +124,9 @@ def get_possible_actions(self) -> frozenset[Action]: # Lazy loading if self.is_done(): return frozenset() - if self._possible_actions is None: - self._possible_actions = frozenset(self.generate_possible_actions()) - return self._possible_actions + if self._possible_heavy_actions is None: + self._possible_heavy_actions = frozenset(self.generate_possible_heavy_actions()) + return self._possible_heavy_actions def check_action(self, action: Action) -> bool: """ @@ -120,16 +138,44 @@ def check_action(self, action: Action) -> bool: Returns: bool: True if the action is feasible, False otherwise. """ - if action in self.get_possible_actions(): - return True + if isinstance(action, LightAction): + return action in self.get_possible_light_actions() + if isinstance(action, HeavyAction): + return action in self.get_possible_heavy_actions() return False @abstractmethod - def convert_light_action_to_action(self,data) -> Action : + def apply_action(self, action: LightAction) -> "GameState": + """ + Applies an action to the game state. + + Args: + action (LightAction): The action to apply. + + Returns: + GameState: The new game state. + + Raises: + MethodNotImplementedError: If the method is not implemented. + """ raise MethodNotImplementedError() + @abstractmethod - def generate_possible_actions(self) -> set[Action]: + def generate_possible_light_actions(self) -> set[LightAction]: + """ + Generates a set of all possible actions from this game state. + + Returns: + Set[Action]: A set of possible actions. + + Raises: + MethodNotImplementedError: If the method is not implemented. + """ + raise MethodNotImplementedError() + + @abstractmethod + def generate_possible_heavy_actions(self) -> set[HeavyAction]: """ Generates a set of all possible actions from this game state. diff --git a/src/seahorse/game/heavy_action.py b/src/seahorse/game/heavy_action.py new file mode 100644 index 0000000..d58f1f2 --- /dev/null +++ b/src/seahorse/game/heavy_action.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from seahorse.game.action import Action +from seahorse.utils.serializer import Serializable + +if TYPE_CHECKING: + from seahorse.game.game_state import GameState + + +class HeavyAction(Action): + """ + A class representing an action in the game. + + Attributes: + past_gs (GameState): The past game state. + new_gs (GameState): The new game state. + """ + + def __init__(self, current_game_state: GameState, next_game_state: GameState) -> None: + """ + Initializes a new instance of the Action class. + + Args: + past_gs (GameState): The past game state. + new_gs (GameState): The new game state. + """ + self.current_game_state = current_game_state + self.next_game_state = next_game_state + + def get_current_game_state(self) -> GameState: + """ + Returns the past game state. + + Returns: + GameState: The past game state. + """ + return self.current_game_state + + def get_next_game_state(self) -> GameState: + """ + Returns the new game state. + + Returns: + GameState: The new game state. + """ + return self.next_game_state + + def get_heavy_action(self, game_state: GameState = None) -> HeavyAction: + """ + Returns the heavy action. + + Returns: + HeavyAction: The heavy action. + """ + return self + + def __hash__(self) -> int: + return hash((hash(self.get_next_game_state()), hash(self.get_current_game_state()))) + + 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__() + + def to_json(self) -> dict: + return self.__dict__ diff --git a/src/seahorse/game/light_action.py b/src/seahorse/game/light_action.py new file mode 100644 index 0000000..21ffabb --- /dev/null +++ b/src/seahorse/game/light_action.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from seahorse.game.action import Action +from seahorse.game.heavy_action import HeavyAction + +if TYPE_CHECKING: + from seahorse.game.game_state import GameState + + +class LightAction(Action): + """ + A class representing an action in the game. + + Attributes: + data (dict): The data of the light action. + """ + + def __init__(self, data: dict) -> None: + """ + Initializes a new instance of the Action class. + + Args: data (dict): The data of the light action. + + """ + self.data = data + + + def get_heavy_action(self, game_state: GameState = None) -> HeavyAction: + """ + Returns the heavy action. + + Returns: + HeavyAction: The heavy action. + """ + if game_state is None: + raise ValueError("Cannot apply a light action without current game state.") + + return HeavyAction(game_state, game_state.apply_action(self)) + + + def __hash__(self) -> int: + return hash(tuple(self.data.items())) + + def __eq__(self, value: object) -> bool: + return hash(self) == hash(value) + + def __str__(self) -> str: + return "LightAction: " + str(self.data) + + def to_json(self) -> dict: + return self.__dict__ diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index d69d5b9..fb31ce2 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -10,7 +10,9 @@ from loguru import logger from seahorse.game.game_state import GameState +from seahorse.game.heavy_action import HeavyAction from seahorse.game.io_stream import EventMaster, EventSlave +from seahorse.game.light_action import LightAction from seahorse.player.player import Player from seahorse.utils.custom_exceptions import ( ActionNotPermittedError, @@ -90,7 +92,8 @@ async def step(self) -> GameState: GamseState : The new game_state. """ next_player = self.current_game_state.get_next_player() - possible_actions = self.current_game_state.get_possible_actions() + + possible_actions = self.current_game_state.get_possible_heavy_actions() start = time.time() # next_player.start_timer() @@ -111,6 +114,7 @@ async def step(self) -> GameState: # next_player.stop_timer() + action = action.get_heavy_action(self.current_game_state) if action not in possible_actions: raise ActionNotPermittedError() @@ -126,6 +130,7 @@ async def play_game(self) -> list[Player]: Returns: Iterable[Player]: The winner(s) of the game. """ + time_start = time.time() await self.emitter.sio.emit( "play", json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()), diff --git a/src/seahorse/player/proxies.py b/src/seahorse/player/proxies.py index 70d4ff9..9435284 100644 --- a/src/seahorse/player/proxies.py +++ b/src/seahorse/player/proxies.py @@ -8,6 +8,7 @@ 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 from seahorse.player.player import Player from seahorse.utils.custom_exceptions import MethodNotImplementedError from seahorse.utils.gui_client import GUIClient @@ -157,12 +158,14 @@ async def play(self, current_state: GameState, **args) -> Action: while True: data = json.loads(await EventMaster.get_instance().wait_for_event(self.sid,"interact",flush_until=time.time())) try: - action = current_state.convert_light_action_to_action(data) + + action = LightAction(data).get_heavy_action(current_state) + except MethodNotImplementedError: #TODO: handle this case action = Action.from_json(data) - if action in current_state.get_possible_actions(): + if action in current_state.get_possible_heavy_actions(): break else: await EventMaster.get_instance().sio.emit("ActionNotPermitted",None) From fc6a3bb0a4b4fc59546e590b56c7f24acaaba7a0 Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Mon, 27 May 2024 21:17:05 -0400 Subject: [PATCH 4/6] add a way to convert gui data to action data --- src/seahorse/game/game_state.py | 13 +++++++++++++ src/seahorse/player/proxies.py | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/seahorse/game/game_state.py b/src/seahorse/game/game_state.py index 6acf73f..45909b4 100644 --- a/src/seahorse/game/game_state.py +++ b/src/seahorse/game/game_state.py @@ -144,6 +144,19 @@ def check_action(self, action: Action) -> bool: return action in self.get_possible_heavy_actions() return False + def convert_gui_data_to_action_data(self, data: dict[str, Any]) -> dict[str, Any]: + """ + Converts GUI data to light action data. + This method can and should be overridden by the user. + + Args: + data (Dict[str, Any]): The GUI data. + + Returns: + Dict[str, Any]: The action data. + """ + return data + @abstractmethod def apply_action(self, action: LightAction) -> "GameState": """ diff --git a/src/seahorse/player/proxies.py b/src/seahorse/player/proxies.py index 9435284..3cd1b8c 100644 --- a/src/seahorse/player/proxies.py +++ b/src/seahorse/player/proxies.py @@ -156,9 +156,9 @@ async def play(self, current_state: GameState, **args) -> Action: if self.shared_sid and not self.sid: self.sid=self.shared_sid.sid while True: - data = json.loads(await EventMaster.get_instance().wait_for_event(self.sid,"interact",flush_until=time.time())) + data_gui = json.loads(await EventMaster.get_instance().wait_for_event(self.sid,"interact",flush_until=time.time())) try: - + data = current_state.convert_gui_data_to_action_data(data_gui) action = LightAction(data).get_heavy_action(current_state) except MethodNotImplementedError: From cce65c8a88f7bd84c812869dd36a2c49e501c83e Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Wed, 5 Jun 2024 15:21:44 -0400 Subject: [PATCH 5/6] [FIX] Remote Timer --- src/seahorse/game/master.py | 26 ++------------------------ src/seahorse/player/player.py | 5 +---- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index fb31ce2..4e585c0 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -44,7 +44,7 @@ def __init__( log_level: str = "INFO", port: int =8080, hostname: str ="localhost", - time_limit: int = 15*60, + time_limit: int = 1e9, ) -> None: """ Initializes a new instance of the GameMaster class. @@ -57,7 +57,6 @@ def __init__( log_level (str): The name of the log file. """ self.timetol = 1e-1 - # self.recorded_plays = [] self.name = name self.current_game_state = initial_game_state self.players = initial_game_state.players @@ -96,9 +95,8 @@ async def step(self) -> GameState: possible_actions = self.current_game_state.get_possible_heavy_actions() start = time.time() - # next_player.start_timer() - logger.info(f"time : {self.remaining_time[next_player.get_id()]}") + 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()]) else: @@ -107,12 +105,6 @@ async def step(self) -> GameState: self.remaining_time[next_player.get_id()] -= (tstp-start) if self.remaining_time[next_player.get_id()] < 0: raise SeahorseTimeoutError() - - # if abs((tstp-start)-(tstp-next_player.get_last_timestamp()))>self.timetol: - # next_player.stop_timer() - # raise StopAndStartError() - - # next_player.stop_timer() action = action.get_heavy_action(self.current_game_state) if action not in possible_actions: @@ -135,9 +127,7 @@ async def play_game(self) -> list[Player]: "play", json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()), ) - # self.recorded_plays.append(self.current_game_state.__class__.from_json(json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()))) id2player={} - # verdict_scores=[-1e9,-1e9] for player in self.get_game_state().get_players() : id2player[player.get_id()]=player.get_name() logger.info(f"Player : {player.get_name()} - {player.get_id()}") @@ -145,31 +135,20 @@ async def play_game(self) -> list[Player]: 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()}") self.current_game_state = await self.step() - # self.recorded_plays.append(self.current_game_state.__class__.from_json(json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()))) except (ActionNotPermittedError,SeahorseTimeoutError,StopAndStartError) as e: if isinstance(e,SeahorseTimeoutError): logger.error(f"Time credit expired for player {self.current_game_state.get_next_player()}") elif isinstance(e,ActionNotPermittedError) : logger.error(f"Action not permitted for player {self.current_game_state.get_next_player()}") - # else: - # logger.error(f"Player {self.current_game_state.get_next_player()} might have tried tampering with the timer.\n The timedelta difference exceeded the allowed tolerancy in GameMaster.timetol ") 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])) temp_score[id_player_error] = -1e9 temp_score[other_player] = 1e9 - - # temp_score.pop(id_player_error) self.winner = self.compute_winner(temp_score) - # self.current_game_state.get_scores()[id_player_error] = -3 - # other_player = next(iter([player.get_id() for player in self.current_game_state.get_players() if player.get_id()!=id_player_error])) - # self.current_game_state.get_scores()[other_player] = 0 - - # scores = self.get_scores() for key in temp_score.keys(): - # verdict_scores[int(id2player[key].split("_")[-1])-1]=-scores[key] logger.info(f"{id2player[key]}:{temp_score[key]}") for player in self.get_winner() : @@ -191,7 +170,6 @@ async def play_game(self) -> list[Player]: self.winner = self.compute_winner(self.current_game_state.get_scores()) scores = self.get_scores() for key in scores.keys() : - # verdict_scores[int(id2player[key].split("_")[-1])-1]=-scores[key] logger.info(f"{id2player[key]}:{(scores[key])}") for player in self.get_winner() : diff --git a/src/seahorse/player/player.py b/src/seahorse/player/player.py index bfe4e30..9daa569 100644 --- a/src/seahorse/player/player.py +++ b/src/seahorse/player/player.py @@ -13,7 +13,7 @@ from seahorse.game.game_state import GameState -class Player(Serializable):#,TimeMixin): +class Player(Serializable): """ A base class representing a player in the game. @@ -28,7 +28,6 @@ def __init__(self, name: str = "bob",*,id:int | None = None,**_) -> None: Args: name (str, optional): The name of the player. Defaults to "bob". - time_limit (float, optional): The time limit for the player's moves. Defaults to 1e6. hard_id (int, optional, keyword-only): Set the player's id in case of distant loading """ self.name = name @@ -36,10 +35,8 @@ def __init__(self, name: str = "bob",*,id:int | None = None,**_) -> None: self.id = builtins.id(self) else: self.id = id - # self.init_timer(time_limit) - # @timed_function def play(self, current_state: GameState, remaining_time: int) -> Action: """ Implements the player's logic and calls compute_action with minimal information. From 4cf5bfeb91789a416a4ea28a57900ce893616c45 Mon Sep 17 00:00:00 2001 From: Loic Grumiaux Date: Wed, 5 Jun 2024 17:58:40 -0400 Subject: [PATCH 6/6] [FIX] Remote Timer --- gen_ref_pages.py | 18 +++--- src/seahorse/game/action.py | 4 +- src/seahorse/game/game_state.py | 6 +- src/seahorse/game/heavy_action.py | 3 +- src/seahorse/game/io_stream.py | 9 +-- src/seahorse/game/light_action.py | 12 ++-- src/seahorse/game/master.py | 6 +- src/seahorse/player/proxies.py | 11 ++-- src/seahorse/utils/custom_exceptions.py | 6 ++ src/seahorse/utils/gui_client.py | 2 +- src/seahorse/utils/recorders.py | 4 +- tests/test_base.py | 4 +- tests/utils/test_timer.py | 78 ------------------------- 13 files changed, 42 insertions(+), 121 deletions(-) delete mode 100644 tests/utils/test_timer.py diff --git a/gen_ref_pages.py b/gen_ref_pages.py index 164173a..ed5958a 100644 --- a/gen_ref_pages.py +++ b/gen_ref_pages.py @@ -5,23 +5,23 @@ import mkdocs_gen_files import re -for path in sorted(Path("src").rglob("*.py")): # +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/game/action.py b/src/seahorse/game/action.py index f78617f..dcc1643 100644 --- a/src/seahorse/game/action.py +++ b/src/seahorse/game/action.py @@ -1,8 +1,6 @@ from __future__ import annotations from abc import abstractmethod -from typing import TYPE_CHECKING - from seahorse.utils.serializer import Serializable class Action(Serializable): @@ -19,7 +17,7 @@ def __init__(self) -> None: pass @abstractmethod - def get_heavy_action(self, **kwargs) -> Action: + def get_heavy_action(self, *_) -> Action: """ Returns the heavy action. diff --git a/src/seahorse/game/game_state.py b/src/seahorse/game/game_state.py index 45909b4..74998c1 100644 --- a/src/seahorse/game/game_state.py +++ b/src/seahorse/game/game_state.py @@ -1,7 +1,6 @@ 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 @@ -112,7 +111,7 @@ def get_possible_light_actions(self) -> frozenset[LightAction]: if self._possible_light_actions is None: self._possible_light_actions = frozenset(self.generate_possible_light_actions()) return self._possible_light_actions - + def get_possible_heavy_actions(self) -> frozenset[HeavyAction]: """ Returns a copy of the possible heavy actions from this state. @@ -172,7 +171,6 @@ def apply_action(self, action: LightAction) -> "GameState": MethodNotImplementedError: If the method is not implemented. """ raise MethodNotImplementedError() - @abstractmethod def generate_possible_light_actions(self) -> set[LightAction]: @@ -186,7 +184,7 @@ def generate_possible_light_actions(self) -> set[LightAction]: MethodNotImplementedError: If the method is not implemented. """ raise MethodNotImplementedError() - + @abstractmethod def generate_possible_heavy_actions(self) -> set[HeavyAction]: """ diff --git a/src/seahorse/game/heavy_action.py b/src/seahorse/game/heavy_action.py index d58f1f2..04235bd 100644 --- a/src/seahorse/game/heavy_action.py +++ b/src/seahorse/game/heavy_action.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING from seahorse.game.action import Action -from seahorse.utils.serializer import Serializable if TYPE_CHECKING: from seahorse.game.game_state import GameState @@ -47,7 +46,7 @@ def get_next_game_state(self) -> GameState: """ return self.next_game_state - def get_heavy_action(self, game_state: GameState = None) -> HeavyAction: + def get_heavy_action(self, *_) -> HeavyAction: """ Returns the heavy action. diff --git a/src/seahorse/game/io_stream.py b/src/seahorse/game/io_stream.py index 451f3d8..ef2e4b4 100644 --- a/src/seahorse/game/io_stream.py +++ b/src/seahorse/game/io_stream.py @@ -13,6 +13,7 @@ from loguru import logger from seahorse.game.action import Action +from seahorse.game.heavy_action import HeavyAction from seahorse.utils.serializer import Serializable if TYPE_CHECKING: @@ -90,8 +91,8 @@ def remote_action(label: str): """ def meta_wrapper(fun: Callable): @functools.wraps(fun) - async def wrapper(self:EventSlave,current_state:GameState,*_,**__): - await EventMaster.get_instance().sio.emit(label,json.dumps(current_state.to_json(),default=lambda x:x.to_json()),to=self.sid) + 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) out = await EventMaster.get_instance().wait_for_next_play(self.sid,current_state.players) return out @@ -248,7 +249,7 @@ async def wait_for_next_play(self,sid:int,players:list) -> Action: new_gs.players = players - return Action(past_gs,new_gs) + return HeavyAction(past_gs,new_gs) async def wait_for_event(self,sid:int,label:str,*,flush_until:float | None=None) -> Coroutine: """Waits for an aribtrary event emitted by the connection identified by `sid` @@ -261,7 +262,7 @@ async def wait_for_event(self,sid:int,label:str,*,flush_until:float | None=None) flush_until (float, optional): The timestamp treshold. Defaults to None. Returns: - Coroutine: a promise yielding the data associated to the event + Coroutine: a promise yielding the data associated to the event """ while not len(self.__events.get(sid,{}).get(label,[])): await asyncio.sleep(.1) diff --git a/src/seahorse/game/light_action.py b/src/seahorse/game/light_action.py index 21ffabb..3b5cc1b 100644 --- a/src/seahorse/game/light_action.py +++ b/src/seahorse/game/light_action.py @@ -4,7 +4,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 @@ -22,11 +22,11 @@ def __init__(self, data: dict) -> None: Initializes a new instance of the Action class. Args: data (dict): The data of the light action. - + """ self.data = data - + def get_heavy_action(self, game_state: GameState = None) -> HeavyAction: """ Returns the heavy action. @@ -35,10 +35,10 @@ def get_heavy_action(self, game_state: GameState = None) -> HeavyAction: HeavyAction: The heavy action. """ if game_state is None: - raise ValueError("Cannot apply a light action without current game state.") - + raise NoGameStateProvidedError() + return HeavyAction(game_state, game_state.apply_action(self)) - + def __hash__(self) -> int: return hash(tuple(self.data.items())) diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index 4e585c0..09d9eb3 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -10,9 +10,7 @@ from loguru import logger from seahorse.game.game_state import GameState -from seahorse.game.heavy_action import HeavyAction from seahorse.game.io_stream import EventMaster, EventSlave -from seahorse.game.light_action import LightAction from seahorse.player.player import Player from seahorse.utils.custom_exceptions import ( ActionNotPermittedError, @@ -110,6 +108,7 @@ 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 @@ -122,7 +121,6 @@ async def play_game(self) -> list[Player]: Returns: Iterable[Player]: The winner(s) of the game. """ - time_start = time.time() await self.emitter.sio.emit( "play", json.dumps(self.current_game_state.to_json(),default=lambda x:x.to_json()), @@ -140,7 +138,6 @@ async def play_game(self) -> list[Player]: logger.error(f"Time credit expired for player {self.current_game_state.get_next_player()}") elif isinstance(e,ActionNotPermittedError) : 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])) @@ -177,7 +174,6 @@ async def play_game(self) -> list[Player]: await self.emitter.sio.emit("done",json.dumps(self.get_scores())) 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: diff --git a/src/seahorse/player/proxies.py b/src/seahorse/player/proxies.py index 3cd1b8c..134afc3 100644 --- a/src/seahorse/player/proxies.py +++ b/src/seahorse/player/proxies.py @@ -98,8 +98,9 @@ def __init__(self, wrapped_player: Player,gs:type=GameState) -> None: async def handle_turn(*data): logger.info(f"{self.wrapped_player.name} is playing") logger.debug(f"Data received : {data}") - logger.debug(f"Deserialized data : \n{gs.from_json(data[0],next_player=self)}") - action = await self.play(gs.from_json(data[0],next_player=self)) + deserialized = json.loads(data[0]) + logger.debug(f"Deserialized data : \n{deserialized}") + action = await self.play(gs.from_json(data[0],next_player=self),remaining_time = deserialized["remaining_time"]) logger.info(f"{self.wrapped_player} played the following action : \n{action}") @self.sio.on("update_id") @@ -118,7 +119,7 @@ def play(self, current_state: GameState, remaining_time: int) -> Action: Returns: Action: The action resulting from the move. """ - return self.compute_action(current_state=current_state, remaining_time=remaining_time) + return self.compute_action(current_state=current_state, remaining_time=remaining_time).get_heavy_action(current_state) def __getattr__(self, attr): return getattr(self.wrapped_player, attr) @@ -136,7 +137,7 @@ def to_json(self) -> dict: return self.wrapped_player.to_json() class InteractivePlayerProxy(LocalPlayerProxy): - """Proxy for interactive players, + """Proxy for interactive players, inherits from `LocalPlayerProxy` """ def __init__(self, mimics: Player, gui_path:Optional[str]=None, *args, **kwargs) -> None: @@ -152,7 +153,7 @@ def __init__(self, mimics: Player, gui_path:Optional[str]=None, *args, **kwargs) self.shared_sid = None self.sid = None - async def play(self, current_state: GameState, **args) -> Action: + async def play(self, current_state: GameState, **_) -> Action: if self.shared_sid and not self.sid: self.sid=self.shared_sid.sid while True: diff --git a/src/seahorse/utils/custom_exceptions.py b/src/seahorse/utils/custom_exceptions.py index 8801cec..1b87d63 100644 --- a/src/seahorse/utils/custom_exceptions.py +++ b/src/seahorse/utils/custom_exceptions.py @@ -15,6 +15,12 @@ def __init__(self, message: str = "Trying to stop something twice !"): self.message = message super().__init__(message) +class NoGameStateProvidedError(Exception): + """Thrown when trying to get a heavy action from a light action without providing a game state + """ + def __init__(self, message: str = "Cannot apply a light action without current game state."): + self.message = message + super().__init__(message) class PlayerDuplicateError(Exception): """Thrown when trying to stop somethin twice """ diff --git a/src/seahorse/utils/gui_client.py b/src/seahorse/utils/gui_client.py index 9cc0547..b07f00a 100644 --- a/src/seahorse/utils/gui_client.py +++ b/src/seahorse/utils/gui_client.py @@ -4,7 +4,7 @@ from typing import Any, Coroutine, Optional from loguru import logger -from seahorse.game.io_stream import EventMaster, EventSlave +from seahorse.game.io_stream import EventMaster, EventSlave class GUIClient(EventSlave): def __init__(self, path:Optional[str]=None) -> None: diff --git a/src/seahorse/utils/recorders.py b/src/seahorse/utils/recorders.py index 03b5503..2017a19 100644 --- a/src/seahorse/utils/recorders.py +++ b/src/seahorse/utils/recorders.py @@ -11,7 +11,7 @@ class StateRecorder(EventSlave): """ def __init__(self) -> None: - super().__init__() + super().__init__() self.identifier = "__REC__"+str(int(time.time()*1000000-random.randint(1,1000000))) self.id = builtins.id(self) self.wrapped_id = self.id @@ -22,7 +22,7 @@ def __init__(self) -> None: @self.sio.on("play") def record_play(data): - self.recorded_content.append(json.loads(data)) + self.recorded_content.append(json.loads(data)) @self.sio.event() def disconnect(): diff --git a/tests/test_base.py b/tests/test_base.py index a1b2486..8722528 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -7,7 +7,7 @@ from seahorse.game.representation import Representation from seahorse.player.player import Player from seahorse.game.game_state import GameState -from seahorse.game.action import Action +from seahorse.game.heavy_action import HeavyAction class Dummy_GameState(GameState): @@ -26,7 +26,7 @@ def generate_possible_actions(self): copy_rep.get_env()[(i, j)] = Piece(piece_type="Added", owner=next_player) list_rep.append(copy.deepcopy(copy_rep)) poss_actions = { - Action( + HeavyAction( self, Dummy_GameState( self.get_scores(), diff --git a/tests/utils/test_timer.py b/tests/utils/test_timer.py deleted file mode 100644 index 2a4cda2..0000000 --- a/tests/utils/test_timer.py +++ /dev/null @@ -1,78 +0,0 @@ -import time -import unittest - -from seahorse.game.time_manager import TimeMixin, timed_function -from seahorse.utils.custom_exceptions import ( - AlreadyRunningError, - NotRunningError, - SeahorseTimeoutError, - TimerNotInitializedError, -) - - -class DummyClass(TimeMixin): - def __init__(self): - self.dummy_attr = "bob" - - @timed_function - def only_before_timeout(self): - return True - - -class MixinTestCase(unittest.TestCase): - - def setUp(self): - self.dummy = DummyClass() - - def test_time_mixin_init_object(self): - assert self.dummy.dummy_attr == "bob" - - def test_timer_not_init(self): - self.assertRaises(TimerNotInitializedError, self.dummy.get_time_limit) - self.assertRaises(TimerNotInitializedError, self.dummy.get_remaining_time) - self.assertRaises(TimerNotInitializedError, self.dummy.start_timer) - self.assertRaises(TimerNotInitializedError, self.dummy.stop_timer) - self.assertRaises(TimerNotInitializedError, self.dummy.is_locked) - self.assertRaises(TimerNotInitializedError, self.dummy.is_running) - self.assertRaises(TimerNotInitializedError, self.dummy.get_last_timestamp) - - def test_time_mixin_init_timer(self): - self.dummy.init_timer(10) - assert self.dummy.get_time_limit() == 10 - assert self.dummy.get_remaining_time() == 10 - assert not self.dummy.is_locked() - assert not self.dummy.is_running() - - def test_time_mixin_start_twice(self): - self.dummy.init_timer(10) - self.dummy.start_timer() - self.assertRaises(AlreadyRunningError, self.dummy.start_timer) - - def test_time_mixin_stop_twice(self): - self.dummy.init_timer(10) - self.dummy.start_timer() - self.dummy.stop_timer() - self.assertRaises(NotRunningError, self.dummy.stop_timer) - - def test_time_lock(self): - - def change_attr(): - self.dummy.dummy_attr = "bob" - - def call_blocked_method(): - return self.dummy.only_before_timeout() - - self.dummy.init_timer(.5) - self.dummy.start_timer() - - time.sleep(.1) - self.dummy.dummy_attr = "marcel" - assert call_blocked_method() - time.sleep(.4) - - self.assertRaises(SeahorseTimeoutError, change_attr) - self.assertRaises(SeahorseTimeoutError, call_blocked_method) - assert self.dummy.is_locked() - - def tearDown(self) -> None: - self.dummy = None