From 4cf5bfeb91789a416a4ea28a57900ce893616c45 Mon Sep 17 00:00:00 2001 From: Loic Grumiaux Date: Wed, 5 Jun 2024 17:58:40 -0400 Subject: [PATCH] [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