From 222a355072ca3cbe70657ad6a3ff3a4de1a0b1b4 Mon Sep 17 00:00:00 2001 From: Loic Grumiaux Date: Wed, 5 Jun 2024 17:58:40 -0400 Subject: [PATCH 1/5] [FIX] Timer + action types for remote communication. --- src/seahorse/game/io_stream.py | 7 ++++--- src/seahorse/game/master.py | 1 + src/seahorse/player/proxies.py | 7 ++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/seahorse/game/io_stream.py b/src/seahorse/game/io_stream.py index 451f3d8..ad0094a 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` diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index efa2f6c..0de8201 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -116,6 +116,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 diff --git a/src/seahorse/player/proxies.py b/src/seahorse/player/proxies.py index 3cd1b8c..1091538 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) From 4d3722d31e412f38e9ff1836ca9a873644a49b42 Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Thu, 6 Jun 2024 08:48:43 -0400 Subject: [PATCH 2/5] fix guthub action --- src/seahorse/game/action.py | 2 - src/seahorse/game/game_state.py | 2 - tests/utils/test_timer.py | 78 --------------------------------- 3 files changed, 82 deletions(-) delete mode 100644 tests/utils/test_timer.py diff --git a/src/seahorse/game/action.py b/src/seahorse/game/action.py index f78617f..2098197 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): diff --git a/src/seahorse/game/game_state.py b/src/seahorse/game/game_state.py index 45909b4..f7ca716 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 @@ -173,7 +172,6 @@ def apply_action(self, action: LightAction) -> "GameState": """ raise MethodNotImplementedError() - @abstractmethod def generate_possible_light_actions(self) -> set[LightAction]: """ 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 From 72de01d68cb800d62edadec8634de0b22cd6c7f0 Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Thu, 6 Jun 2024 08:58:37 -0400 Subject: [PATCH 3/5] remove white spaces --- gen_ref_pages.py | 18 +++++++++--------- src/seahorse/game/game_state.py | 6 +++--- src/seahorse/game/heavy_action.py | 3 +-- src/seahorse/game/io_stream.py | 2 +- src/seahorse/game/light_action.py | 8 ++++---- src/seahorse/game/master.py | 6 +++--- src/seahorse/player/proxies.py | 2 +- src/seahorse/utils/gui_client.py | 2 +- src/seahorse/utils/recorders.py | 4 ++-- 9 files changed, 25 insertions(+), 26 deletions(-) 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/game_state.py b/src/seahorse/game/game_state.py index f7ca716..74998c1 100644 --- a/src/seahorse/game/game_state.py +++ b/src/seahorse/game/game_state.py @@ -111,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. @@ -171,7 +171,7 @@ 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]: """ @@ -184,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..01d7664 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 ad0094a..ef2e4b4 100644 --- a/src/seahorse/game/io_stream.py +++ b/src/seahorse/game/io_stream.py @@ -262,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..8a234ef 100644 --- a/src/seahorse/game/light_action.py +++ b/src/seahorse/game/light_action.py @@ -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. @@ -36,9 +36,9 @@ def get_heavy_action(self, game_state: GameState = None) -> HeavyAction: """ 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())) diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index 0de8201..c47477d 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -105,7 +105,7 @@ 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() @@ -146,7 +146,7 @@ async def play_game(self) -> list[Player]: 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()}") @@ -201,7 +201,7 @@ 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") - print(f"Time taken for the whole game : {time.time()-time_start}") + print(f"Time taken for the whole game : {time.time()-time_start}") return self.winner diff --git a/src/seahorse/player/proxies.py b/src/seahorse/player/proxies.py index 1091538..35239aa 100644 --- a/src/seahorse/player/proxies.py +++ b/src/seahorse/player/proxies.py @@ -137,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: 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(): From 4d7672864e80e78a1fe75537496ff69b8ec5c630 Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Thu, 6 Jun 2024 09:53:50 -0400 Subject: [PATCH 4/5] fixing github action --- src/seahorse/game/light_action.py | 4 ++-- src/seahorse/game/master.py | 5 ----- src/seahorse/player/proxies.py | 2 +- src/seahorse/utils/custom_exceptions.py | 6 ++++++ tests/test_base.py | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/seahorse/game/light_action.py b/src/seahorse/game/light_action.py index 8a234ef..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 @@ -35,7 +35,7 @@ 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)) diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index c47477d..2b39228 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, @@ -200,9 +198,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") - - print(f"Time taken for the whole game : {time.time()-time_start}") - 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 35239aa..134afc3 100644 --- a/src/seahorse/player/proxies.py +++ b/src/seahorse/player/proxies.py @@ -153,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/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(), From 22ab8305a367017d798af389ad8e6ff49dd45c88 Mon Sep 17 00:00:00 2001 From: yoaaaaaaaann Date: Thu, 6 Jun 2024 09:56:00 -0400 Subject: [PATCH 5/5] cette fois c'est la bonne --- src/seahorse/game/master.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/seahorse/game/master.py b/src/seahorse/game/master.py index 2b39228..697bafc 100644 --- a/src/seahorse/game/master.py +++ b/src/seahorse/game/master.py @@ -127,14 +127,11 @@ 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()), ) - # 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()}") @@ -142,33 +139,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() :