diff --git a/src/mind_reader/domain/session.py b/src/mind_reader/domain/session.py index b3fc553..d63db66 100644 --- a/src/mind_reader/domain/session.py +++ b/src/mind_reader/domain/session.py @@ -6,11 +6,13 @@ from typing import Optional class ArrowDirection(StrEnum): LEFT = auto() RIGHT = auto() + HIDDEN = auto() class StepState(Enum): WAITING_FOR_SETTER = auto() WAITING_FOR_GUESSER = auto() + WAITING_FOR_CLOSURE = auto() SESSION_FINISHED = auto() diff --git a/src/mind_reader/input/session_input.py b/src/mind_reader/input/session_input.py index f640fe2..03ea51b 100644 --- a/src/mind_reader/input/session_input.py +++ b/src/mind_reader/input/session_input.py @@ -1,4 +1,4 @@ -from ..domain import ArrowDirection, Player +from ..domain import ArrowDirection, Player, PlayerRole from ..network import NetworkController, NetworkMessage, NetworkMessageKind @@ -13,16 +13,58 @@ class SessionInput: ) def player_move(self, direction: ArrowDirection) -> None: - payload = { - "player": { - "name": self._local_player.name, - "role": self._local_player.role, - }, - "arrow_direction": direction, - } - self._network.send( - NetworkMessage(NetworkMessageKind.SESSION_PLAYER_MOVED, payload) - ) + match self._local_player.role: + case PlayerRole.SETTER: + local_message = NetworkMessage( + NetworkMessageKind.SESSION_PLAYER_MOVED, + { + "player": { + "name": self._local_player.name, + "role": self._local_player.role, + }, + "arrow_direction": direction, + }, + ) + + network_message = NetworkMessage( + NetworkMessageKind.SESSION_PLAYER_MOVED, + { + "player": { + "name": self._local_player.name, + "role": self._local_player.role, + }, + "arrow_direction": ArrowDirection.HIDDEN, + }, + local_message.id, + ) + + self._network.send_with_looback(local_message, network_message) + + case PlayerRole.GUESSER: + self._network.send( + NetworkMessage( + NetworkMessageKind.SESSION_PLAYER_MOVED, + { + "player": { + "name": self._local_player.name, + "role": self._local_player.role, + }, + "arrow_direction": direction, + }, + ) + ) + + self._network.send( + NetworkMessage( + NetworkMessageKind.SESSION_SECRET_REVEAL_REQUESTED, + { + "player": { + "name": self._local_player.name, + "role": self._local_player.role, + }, + }, + ) + ) def finish_session(self) -> None: pass diff --git a/src/mind_reader/network/controller.py b/src/mind_reader/network/controller.py index cbbf983..bd06629 100644 --- a/src/mind_reader/network/controller.py +++ b/src/mind_reader/network/controller.py @@ -12,6 +12,7 @@ MessageHandler = Callable[[NetworkMessage], None] class NetworkController: def __init__(self, host: str, port: int, username: str, password: str): self._handlers: set[MessageHandler] = set() + self._pending_loopbacks: dict[str, NetworkMessage] = {} self._topic = "mind_reader" self._client = mqtt.Client(CallbackAPIVersion.VERSION2) @@ -40,12 +41,28 @@ class NetworkController: match kind: case ( NetworkMessageKind.SESSION_PLAYER_MOVED + | NetworkMessageKind.SESSION_SECRET_REVEAL_REQUESTED + | NetworkMessageKind.SESSION_SECRET_REVEALED | NetworkMessageKind.SESSION_SESSION_FINISHED | NetworkMessageKind.SESSION_SESSION_STARTED ): topic = f"{self._topic}/session" self._client.publish(topic, payload) + def send_with_looback( + self, + local_message: NetworkMessage, + network_message: NetworkMessage, + ) -> None: + if local_message.id != network_message.id: + raise ValueError( + f"Mismatched message IDs in send_with_loopback: " + f"local_message.id='{local_message.id}' vs network_message.id='{network_message.id}'" + ) + + self._pending_loopbacks[local_message.id] = local_message + self.send(network_message) + def _handle(self, network_message: NetworkMessage) -> None: for handler in self._handlers: handler(network_message) @@ -54,5 +71,10 @@ class NetworkController: self, client: mqtt.Client, userdata: None, message: mqtt.MQTTMessage ) -> None: json_str = message.payload.decode("utf-8") - network_message = NetworkMessage.from_json(json_str) - self._handle(network_message) + incoming_message = NetworkMessage.from_json(json_str) + + if incoming_message.id in self._pending_loopbacks: + local_message = self._pending_loopbacks.pop(incoming_message.id) + self._handle(local_message) + else: + self._handle(incoming_message) diff --git a/src/mind_reader/network/enums.py b/src/mind_reader/network/enums.py index 38756cf..1354532 100644 --- a/src/mind_reader/network/enums.py +++ b/src/mind_reader/network/enums.py @@ -7,6 +7,8 @@ class NetworkMessageKind(StrEnum): # ---- Session ---- SESSION_SESSION_STARTED = "session_started" SESSION_PLAYER_MOVED = "player_moved" + SESSION_SECRET_REVEAL_REQUESTED = "secret_reveal_requested" + SESSION_SECRET_REVEALED = "secret_revealed" SESSION_SESSION_FINISHED = "session_finished" # ---- System ----- diff --git a/src/mind_reader/network/handlers.py b/src/mind_reader/network/handlers.py index b35795f..602147d 100644 --- a/src/mind_reader/network/handlers.py +++ b/src/mind_reader/network/handlers.py @@ -1,11 +1,13 @@ from ..domain import ArrowDirection, Player, PlayerRole from ..state import SessionState +from .controller import NetworkController from .enums import NetworkMessageKind from .message import NetworkMessage class SessionNetworkMessageHandler: - def __init__(self, state: SessionState): + def __init__(self, network: NetworkController, state: SessionState): + self._network = network self._state = state def __call__(self, message: NetworkMessage): @@ -23,5 +25,23 @@ class SessionNetworkMessageHandler: case PlayerRole.GUESSER: self._state.guesser_move(direction) + case NetworkMessageKind.SESSION_SECRET_REVEAL_REQUESTED: + self._network.send( + NetworkMessage( + NetworkMessageKind.SESSION_SECRET_REVEALED, + { + "player": { + "name": self._state.local_player.name, + "role": self._state.local_player.role, + }, + "arrow_direction": self._state.reveal_setter_move(), + }, + ) + ) + + case NetworkMessageKind.SESSION_SECRET_REVEALED: + direction = ArrowDirection(message.payload["arrow_direction"]) + self._state.step_close(direction) + case NetworkMessageKind.SESSION_SESSION_FINISHED: pass diff --git a/src/mind_reader/state/session_state.py b/src/mind_reader/state/session_state.py index 627ffbe..5d12aa4 100644 --- a/src/mind_reader/state/session_state.py +++ b/src/mind_reader/state/session_state.py @@ -112,12 +112,21 @@ class SessionState: return False self._guesser_moves.append(direction) + self._step_state = StepState.WAITING_FOR_CLOSURE + return True + + def step_close(self, direction: ArrowDirection) -> bool: + if self._step_state != StepState.WAITING_FOR_CLOSURE: + return False if self._completed_move_count < self._sequence_length: self._step_state = StepState.WAITING_FOR_SETTER else: self._step_state = StepState.SESSION_FINISHED + if self._local_player.role == PlayerRole.GUESSER: + self._setter_moves[-1] = direction + index = len(self._guesser_moves) - 1 pub.sendMessage( @@ -157,6 +166,13 @@ class SessionState: return True + def reveal_setter_move(self) -> ArrowDirection: + if self._local_player.role == PlayerRole.SETTER: + if self._step_state == StepState.WAITING_FOR_CLOSURE: + return self._setter_moves[-1] + + return ArrowDirection.HIDDEN + def local_move(self, direction: ArrowDirection) -> bool: if not self.can_local_move(): return False @@ -171,6 +187,8 @@ class SessionState: return self._local_player.role == PlayerRole.SETTER case StepState.WAITING_FOR_GUESSER: return self._local_player.role == PlayerRole.GUESSER + case StepState.WAITING_FOR_CLOSURE: + return False case StepState.SESSION_FINISHED: return False diff --git a/src/mind_reader/ui/views/session_view.py b/src/mind_reader/ui/views/session_view.py index 3784b31..10e2820 100644 --- a/src/mind_reader/ui/views/session_view.py +++ b/src/mind_reader/ui/views/session_view.py @@ -29,6 +29,7 @@ class GuesserCardColour: class ArrowSymbol: LEFT: str = "←" RIGHT: str = "→" + HIDDEN: str = "?" class Card(wx.Panel): @@ -61,7 +62,7 @@ class Card(wx.Panel): case CardState.SETTER_HIDDEN: colour = SetterCardColour.HIDDEN - show_arrow = False + show_arrow = True case CardState.SETTER_REVEALED: colour = SetterCardColour.REVEALED @@ -91,6 +92,9 @@ class Card(wx.Panel): case ArrowDirection.RIGHT: symbol = ArrowSymbol.RIGHT + case ArrowDirection.HIDDEN: + symbol = ArrowSymbol.HIDDEN + self.label.SetLabel(symbol) else: