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 9f1cc09..dcc1643 100644 --- a/src/seahorse/game/action.py +++ b/src/seahorse/game/action.py @@ -1,59 +1,28 @@ from __future__ import annotations -from typing import TYPE_CHECKING - +from abc import abstractmethod 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 + pass - def get_current_game_state(self) -> GameState: + @abstractmethod + def get_heavy_action(self, *_) -> Action: """ - Returns the past game state. + Returns the heavy action. Returns: - GameState: The past game state. + Action: The heavy action. """ - 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 __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..74998c1 100644 --- a/src/seahorse/game/game_state.py +++ b/src/seahorse/game/game_state.py @@ -1,8 +1,9 @@ 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 from seahorse.game.representation import Representation from seahorse.player.player import Player from seahorse.utils.custom_exceptions import MethodNotImplementedError @@ -34,7 +35,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 +97,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 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 actions from this state. - The first call triggers the `generate_possible_actions` method. + 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 +123,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 +137,56 @@ 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 + 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 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_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_actions(self) -> set[Action]: + 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..04235bd --- /dev/null +++ b/src/seahorse/game/heavy_action.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from seahorse.game.action import Action + +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, *_) -> 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/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 new file mode 100644 index 0000000..3b5cc1b --- /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 +from seahorse.utils.custom_exceptions import NoGameStateProvidedError +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 NoGameStateProvidedError() + + 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 42a4d96..09d9eb3 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 = 1e9, ) -> None: """ Initializes a new instance of the GameMaster class. @@ -54,10 +55,11 @@ 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 + 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: 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() - logger.info(f"time : {next_player.get_remaining_time()}") + 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() - - next_player.stop_timer() + self.remaining_time[next_player.get_id()] -= (tstp-start) + if self.remaining_time[next_player.get_id()] < 0: + raise SeahorseTimeoutError() + action = action.get_heavy_action(self.current_game_state) 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 @@ -124,9 +125,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()}") @@ -134,33 +133,28 @@ 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() - 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 + self.winner = self.compute_winner(temp_score) + + for key in temp_score.keys(): + 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 +167,13 @@ 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() : 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..9daa569 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): """ A base class representing a player in the game. @@ -22,13 +22,12 @@ 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. 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,11 +35,9 @@ 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) - @timed_function - def play(self, current_state: GameState) -> Action: + def play(self, current_state: GameState, remaining_time: int) -> Action: """ Implements the player's logic and calls compute_action with minimal information. @@ -54,7 +51,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..134afc3 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 @@ -38,7 +39,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. @@ -97,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") @@ -107,7 +109,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 +119,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).get_heavy_action(current_state) def __getattr__(self, attr): return getattr(self.wrapped_player, attr) @@ -129,13 +131,13 @@ 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() 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: @@ -151,18 +153,20 @@ 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, **_) -> 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: - action = current_state.convert_light_action_to_action(data) + data = current_state.convert_gui_data_to_action_data(data_gui) + 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) 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..5b26823 100644 --- a/src/seahorse/utils/gui_client.py +++ b/src/seahorse/utils/gui_client.py @@ -1,10 +1,11 @@ import builtins import os import subprocess +import platform 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: @@ -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") 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