11 Commits
Author SHA1 Message Date
vladimir 4533789432 feat: переработка NetwrokMessage и EventMapper 2026-08-25 01:50:45 +03:00
vladimir b1132715e1 feat: наследование от BaseEvent и метод from_payload для каждого из классов Event 2026-08-25 01:49:12 +03:00
vladimir 878b83dc1c feat: метод from_dict для класса Player 2026-08-25 01:48:02 +03:00
vladimir 28030412d0 feat: базовый класс BaseEvent с универсальным методом to_payload для гарантированного преобразования всех данных в строки 2026-08-25 01:46:58 +03:00
vladimir 6017c2d757 fix: переработка методов NetworkController 2026-08-24 01:43:03 +03:00
vladimir 53be71316c fix: удаление ненужных методов 2026-08-23 21:06:02 +03:00
vladimir 473b03c1bb fix: корректное определение текущего индекса в методах возврата состояния карт 2026-08-23 21:06:02 +03:00
vladimir a07d94bcc4 fix: корректное определение текущего индекса 2026-08-23 21:05:51 +03:00
vladimir 9a4c98fb76 wip: без комментариев 2026-08-23 01:49:02 +03:00
vladimir 5c7966755f wip: возможность получения загаданной стрелки только после попытки отгадки 2026-08-19 04:06:30 +03:00
vladimir 0730d8180b fix: исправлен порядок открытия карт у загадывающего 2026-08-19 01:39:39 +03:00
19 changed files with 645 additions and 118 deletions
+14
View File
@@ -4,7 +4,14 @@ from .session import (
ArrowDirection, ArrowDirection,
CardData, CardData,
CardState, CardState,
GuesserMoveEvent,
NextStepRequestedEvent,
PlayerHelloEvent,
RevealSecretEvent,
SessionEvent,
SessionPhase,
SessionStats, SessionStats,
SetterMoveEvent,
StepData, StepData,
StepState, StepState,
) )
@@ -16,7 +23,14 @@ __all__ = [
"ArrowDirection", "ArrowDirection",
"CardData", "CardData",
"CardState", "CardState",
"GuesserMoveEvent",
"NextStepRequestedEvent",
"PlayerHelloEvent",
"RevealSecretEvent",
"SessionEvent",
"SessionPhase",
"SessionStats", "SessionStats",
"SetterMoveEvent",
"StepData", "StepData",
"StepState", "StepState",
] ]
+1 -1
View File
@@ -7,7 +7,7 @@ class PlayerRole(StrEnum):
GUESSER = auto() GUESSER = auto()
@dataclass @dataclass(frozen=True)
class Player: class Player:
name: str name: str
role: PlayerRole role: PlayerRole
+51
View File
@@ -2,20 +2,34 @@ from dataclasses import dataclass
from enum import Enum, StrEnum, auto from enum import Enum, StrEnum, auto
from typing import Optional from typing import Optional
from .common import Player
class ArrowDirection(StrEnum): class ArrowDirection(StrEnum):
LEFT = auto() LEFT = auto()
RIGHT = auto() RIGHT = auto()
HIDDEN = auto()
class StepState(Enum): class StepState(Enum):
WAITING_FOR_SETTER = auto() WAITING_FOR_SETTER = auto()
WAITING_FOR_GUESSER = auto() WAITING_FOR_GUESSER = auto()
WAITING_FOR_CLOSURE = auto()
SESSION_FINISHED = auto() SESSION_FINISHED = auto()
class SessionPhase(Enum):
INITIALIZING = auto()
WAITING_FOR_SETTER = auto()
WAITING_FOR_GUESSER = auto()
WAITING_FOR_REVEAL = auto()
CHECKOUT = auto()
FINISHED = auto()
class CardState(Enum): class CardState(Enum):
SETTER_NOT_SET = auto() SETTER_NOT_SET = auto()
SETTER_WAITING = auto()
SETTER_HIDDEN = auto() SETTER_HIDDEN = auto()
SETTER_REVEALED = auto() SETTER_REVEALED = auto()
GUESSER_DISABLED = auto() GUESSER_DISABLED = auto()
@@ -52,3 +66,40 @@ class SessionStats:
accuracy = 0.0 accuracy = 0.0
accuracy = (self.correct / self.total) * 100 accuracy = (self.correct / self.total) * 100
return round(accuracy, 1) return round(accuracy, 1)
@dataclass(frozen=True)
class PlayerHelloEvent:
player: Player
@dataclass(frozen=True)
class SetterMoveEvent:
player: Player
direction: ArrowDirection
@dataclass(frozen=True)
class GuesserMoveEvent:
player: Player
direction: ArrowDirection
@dataclass(frozen=True)
class RevealSecretEvent:
player: Player
direction: ArrowDirection
@dataclass(frozen=True)
class NextStepRequestedEvent:
player: Player
SessionEvent = (
PlayerHelloEvent
| SetterMoveEvent
| GuesserMoveEvent
| RevealSecretEvent
| NextStepRequestedEvent
)
+53 -11
View File
@@ -1,4 +1,4 @@
from ..domain import ArrowDirection, Player from ..domain import ArrowDirection, Player, PlayerRole
from ..network import NetworkController, NetworkMessage, NetworkMessageKind from ..network import NetworkController, NetworkMessage, NetworkMessageKind
@@ -13,16 +13,58 @@ class SessionInput:
) )
def player_move(self, direction: ArrowDirection) -> None: def player_move(self, direction: ArrowDirection) -> None:
payload = { match self._local_player.role:
"player": { case PlayerRole.SETTER:
"name": self._local_player.name, local_message = NetworkMessage(
"role": self._local_player.role, NetworkMessageKind.SESSION_PLAYER_MOVED,
}, {
"arrow_direction": direction, "player": {
} "name": self._local_player.name,
self._network.send( "role": self._local_player.role,
NetworkMessage(NetworkMessageKind.SESSION_PLAYER_MOVED, payload) },
) "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: def finish_session(self) -> None:
pass pass
+10 -10
View File
@@ -1,11 +1,11 @@
from .controller import NetworkController # from .controller import NetworkController
from .enums import NetworkMessageKind # from .enums import NetworkMessageKind
from .handlers import SessionNetworkMessageHandler # from .handlers import SessionNetworkMessageHandler
from .message import NetworkMessage # from .message import NetworkMessage
__all__ = [ # __all__ = [
"NetworkController", # "NetworkController",
"NetworkMessageKind", # "NetworkMessageKind",
"SessionNetworkMessageHandler", # "SessionNetworkMessageHandler",
"NetworkMessage", # "NetworkMessage",
] # ]
+84 -32
View File
@@ -1,24 +1,48 @@
from typing import Callable import logging
from typing import Optional
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from paho.mqtt.enums import CallbackAPIVersion from paho.mqtt.enums import CallbackAPIVersion
from .enums import NetworkMessageKind from ..state_machines.session.events import SessionEvent
from .message import NetworkMessage from ..state_machines.session.machine import SessionStateMachine
from .message import EventMapper, NetworkMessage, SessionMessageKind
MessageHandler = Callable[[NetworkMessage], None] logger = logging.getLogger(__name__)
class NetworkController: class NetworkController:
def __init__(self, host: str, port: int, username: str, password: str): def __init__(
self._handlers: set[MessageHandler] = set() self,
host: str,
port: int,
username: str,
password: str,
lobby,
session: SessionStateMachine,
):
self._lobby_topic = "mind_reader/lobby"
self._session_topic = "mind_reader/session"
self._lobby = lobby
self._session = session
self._pending_loopbacks: dict[str, NetworkMessage] = {}
self._topic = "mind_reader"
self._client = mqtt.Client(CallbackAPIVersion.VERSION2) self._client = mqtt.Client(CallbackAPIVersion.VERSION2)
self._client.username_pw_set(username, password) self._client.username_pw_set(username, password)
self._client.on_message = self._on_message
self._client.message_callback_add(
self._lobby_topic, self._on_lobby_message
)
self._client.message_callback_add(
self._session_topic, self._on_session_message
)
self._client.connect(host, port) self._client.connect(host, port)
self._client.subscribe("mind_reader/#")
self._client.subscribe(self._lobby_topic)
self._client.subscribe(self._session_topic)
def start(self) -> None: def start(self) -> None:
self._client.loop_start() self._client.loop_start()
@@ -27,32 +51,60 @@ class NetworkController:
self._client.loop_stop() self._client.loop_stop()
self._client.disconnect() self._client.disconnect()
def add_handler(self, handler: MessageHandler) -> None:
self._handlers.add(handler)
def remove_handler(self, handler: MessageHandler) -> None:
self._handlers.remove(handler)
def send(self, network_message: NetworkMessage) -> None: def send(self, network_message: NetworkMessage) -> None:
kind = network_message.kind match network_message.kind:
payload = network_message.to_json() case SessionMessageKind():
self._client.publish(
self._session_topic, network_message.to_json()
)
match kind: def send_with_loopback(
case ( self,
NetworkMessageKind.SESSION_PLAYER_MOVED local_message: NetworkMessage,
| NetworkMessageKind.SESSION_SESSION_FINISHED network_message: NetworkMessage,
| NetworkMessageKind.SESSION_SESSION_STARTED ) -> None:
): if local_message.id != network_message.id:
topic = f"{self._topic}/session" raise ValueError(
self._client.publish(topic, payload) f"Mismatched message IDs in send_with_loopback: "
f"local_message.id='{local_message.id}'"
f"vs network_message.id='{network_message.id}'"
)
def _handle(self, network_message: NetworkMessage) -> None: self._pending_loopbacks[local_message.id] = local_message
for handler in self._handlers: self.send(network_message)
handler(network_message)
def _on_message( def _pop_loopback(self, message_id: str) -> Optional[NetworkMessage]:
return self._pending_loopbacks.pop(message_id, None)
def _parse_incoming_message(
self, message: mqtt.MQTTMessage
) -> Optional[NetworkMessage]:
try:
json_str = message.payload.decode()
network_message = NetworkMessage.from_json(json_str)
except Exception as err:
logger.error("Failed to parse incoming MQTT message: %s", err)
return None
loopback_message = self._pop_loopback(network_message.id)
return loopback_message or network_message
def _on_lobby_message(
self, client: mqtt.Client, userdata: None, message: mqtt.MQTTMessage self, client: mqtt.Client, userdata: None, message: mqtt.MQTTMessage
) -> None: ) -> None:
json_str = message.payload.decode("utf-8") network_message = self._parse_incoming_message(message)
network_message = NetworkMessage.from_json(json_str) if network_message is None:
self._handle(network_message) return
event = EventMapper.from_message(network_message)
# if isinstance(event, LobbyEvent):
# self._lobby.process(event)
def _on_session_message(
self, client: mqtt.Client, userdata: None, message: mqtt.MQTTMessage
) -> None:
network_message = self._parse_incoming_message(message)
if network_message is None:
return
event = EventMapper.from_message(network_message)
if isinstance(event, SessionEvent):
self._session.process(event)
-13
View File
@@ -1,13 +0,0 @@
from enum import StrEnum
class NetworkMessageKind(StrEnum):
# ----- Lobby -----
# ---- Session ----
SESSION_SESSION_STARTED = "session_started"
SESSION_PLAYER_MOVED = "player_moved"
SESSION_SESSION_FINISHED = "session_finished"
# ---- System -----
SYSTEM_INC_MSG_PA_ERR = "incoming_message_parsing_error"
-27
View File
@@ -1,27 +0,0 @@
from ..domain import ArrowDirection, Player, PlayerRole
from ..state import SessionState
from .enums import NetworkMessageKind
from .message import NetworkMessage
class SessionNetworkMessageHandler:
def __init__(self, state: SessionState):
self._state = state
def __call__(self, message: NetworkMessage):
match message.kind:
case NetworkMessageKind.SESSION_SESSION_STARTED:
self._state.start_session()
case NetworkMessageKind.SESSION_PLAYER_MOVED:
player_role = PlayerRole(message.payload["player"]["role"])
direction = ArrowDirection(message.payload["arrow_direction"])
match player_role:
case PlayerRole.SETTER:
self._state.setter_move(direction)
case PlayerRole.GUESSER:
self._state.guesser_move(direction)
case NetworkMessageKind.SESSION_SESSION_FINISHED:
pass
+86 -12
View File
@@ -1,34 +1,108 @@
import json import json
import logging
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from enum import StrEnum, auto
from typing import Any, Optional from typing import Any, Optional
from uuid_extensions import uuid7str from uuid_extensions import uuid7str
from .enums import NetworkMessageKind from ..state_machines.lobby.domain import Player, PlayerRole
from ..state_machines.session.domain import ArrowDirection
from ..state_machines.session.events import (
GuesserMoveEvent,
NextStepRequestedEvent,
PlayerHelloEvent,
RevealSecretEvent,
SessionEvent,
SetterMoveEvent,
)
logger = logging.getLogger(__name__)
class SessionMessageKind(StrEnum):
PLAYER_HELLO = auto()
SETTER_MOVED = auto()
GUESSER_MOVED = auto()
SECRET_REVEALED = auto()
NEXT_STEP_REQUESTED = auto()
NetworkMessageKind = SessionMessageKind
Event = SessionEvent
EVENT_TO_KIND_MAP: dict[type[Event], NetworkMessageKind] = {
PlayerHelloEvent: SessionMessageKind.PLAYER_HELLO,
SetterMoveEvent: SessionMessageKind.SETTER_MOVED,
GuesserMoveEvent: SessionMessageKind.GUESSER_MOVED,
RevealSecretEvent: SessionMessageKind.SECRET_REVEALED,
NextStepRequestedEvent: SessionMessageKind.NEXT_STEP_REQUESTED,
}
KIND_TO_EVENT_MAP = {
kind: event_cls for event_cls, kind in EVENT_TO_KIND_MAP.items()
}
def serializer(obj: Any) -> Any:
if isinstance(obj, StrEnum):
return obj.value
raise TypeError(
f"Object of type {type(obj).__name__} is not JSON serializable"
)
@dataclass(frozen=True) @dataclass(frozen=True)
class NetworkMessage: class NetworkMessage:
kind: NetworkMessageKind kind: NetworkMessageKind
payload: dict[str, Any] payload: dict[str, Any] = field(default_factory=dict)
id: str = field(default_factory=uuid7str) id: str = field(default_factory=uuid7str)
def to_json(self) -> str:
data = asdict(self)
data["kind"] = self.kind.value
return json.dumps(data)
@classmethod @classmethod
def from_json(cls, json_str: str) -> "NetworkMessage": def from_json(cls, json_str: str) -> "NetworkMessage":
try: try:
data = json.loads(json_str) data = json.loads(json_str)
return cls( return cls(
id=data["id"],
kind=NetworkMessageKind(data["kind"]), kind=NetworkMessageKind(data["kind"]),
payload=data.get("payload", {}), payload=data.get("payload", {}),
id=data["id"],
) )
except (json.JSONDecodeError, KeyError, ValueError) as e: except (KeyError, ValueError, json.JSONDecodeError) as err:
return cls( logger.error(
kind=NetworkMessageKind.SYSTEM_INC_MSG_PA_ERR, "Failed to deserialize NetworkMessage from JSON: %s", err
payload={"exception": e},
) )
raise
def to_json(self) -> str:
data = {"kind": self.kind.value, "payload": self.payload, "id": self.id}
return json.dumps(data, ensure_ascii=False, default=serializer)
class EventMapper:
@staticmethod
def to_message(event: Event) -> NetworkMessage:
event_type = type(event)
kind = EVENT_TO_KIND_MAP.get(event_type)
if not kind:
logger.error(
"Attempted to serialize unregistered event: %s", event_type
)
raise ValueError(
f"Event {event_type} is not registered in EVENT_TO_KIND_MAP"
)
return NetworkMessage(kind=kind, payload=event.to_payload())
@staticmethod
def from_message(message: NetworkMessage) -> Event | None:
event_cls = KIND_TO_EVENT_MAP.get(message.kind)
if not event_cls:
return None
try:
return event_cls.from_payload(message.payload)
except (KeyError, ValueError, TypeError):
return None
+28 -11
View File
@@ -32,27 +32,26 @@ class SessionState:
return len(self._guesser_moves) return len(self._guesser_moves)
def _get_setter_card_state(self, index: int) -> CardState: def _get_setter_card_state(self, index: int) -> CardState:
current_index = len(self._setter_moves) completed = self._completed_move_count
if index > current_index: if index > completed:
return CardState.SETTER_NOT_SET return CardState.SETTER_NOT_SET
elif index == current_index: elif index == completed:
if self._step_state == StepState.WAITING_FOR_GUESSER: if self._local_player.role == PlayerRole.SETTER:
if self._local_player.role == PlayerRole.SETTER: return CardState.SETTER_REVEALED
return CardState.SETTER_REVEALED else:
else: return CardState.SETTER_HIDDEN
return CardState.SETTER_HIDDEN
return CardState.SETTER_REVEALED return CardState.SETTER_REVEALED
def _get_guesser_card_state(self, index: int) -> CardState: def _get_guesser_card_state(self, index: int) -> CardState:
completed_move_count = self._completed_move_count completed = self._completed_move_count
if index > completed_move_count: if index > completed:
return CardState.GUESSER_DISABLED return CardState.GUESSER_DISABLED
elif index == completed_move_count: elif index == completed:
if self._step_state == StepState.WAITING_FOR_GUESSER: if self._step_state == StepState.WAITING_FOR_GUESSER:
return CardState.GUESSER_WAITING return CardState.GUESSER_WAITING
else: else:
@@ -113,12 +112,21 @@ class SessionState:
return False return False
self._guesser_moves.append(direction) 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: if self._completed_move_count < self._sequence_length:
self._step_state = StepState.WAITING_FOR_SETTER self._step_state = StepState.WAITING_FOR_SETTER
else: else:
self._step_state = StepState.SESSION_FINISHED self._step_state = StepState.SESSION_FINISHED
if self._local_player.role == PlayerRole.GUESSER:
self._setter_moves[-1] = direction
index = len(self._guesser_moves) - 1 index = len(self._guesser_moves) - 1
pub.sendMessage( pub.sendMessage(
@@ -158,6 +166,13 @@ class SessionState:
return True 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: def local_move(self, direction: ArrowDirection) -> bool:
if not self.can_local_move(): if not self.can_local_move():
return False return False
@@ -172,6 +187,8 @@ class SessionState:
return self._local_player.role == PlayerRole.SETTER return self._local_player.role == PlayerRole.SETTER
case StepState.WAITING_FOR_GUESSER: case StepState.WAITING_FOR_GUESSER:
return self._local_player.role == PlayerRole.GUESSER return self._local_player.role == PlayerRole.GUESSER
case StepState.WAITING_FOR_CLOSURE:
return False
case StepState.SESSION_FINISHED: case StepState.SESSION_FINISHED:
return False return False
@@ -0,0 +1,22 @@
from dataclasses import asdict, dataclass, is_dataclass
from enum import StrEnum
from typing import Any
def _factory(d: list[tuple[str, Any]]) -> dict[str, Any]:
def _convert(obj: Any) -> Any:
if isinstance(obj, StrEnum):
return obj.value
if isinstance(obj, list | tuple):
return [_convert(item) for item in obj]
if isinstance(obj, dict):
return {k: _convert(v) for k, v in obj.items()}
return obj
return {k: _convert(v) for k, v in d}
@dataclass(frozen=True)
class BaseEvent:
def to_payload(self) -> dict[str, Any]:
return asdict(self, dict_factory=_factory)
@@ -0,0 +1,19 @@
from dataclasses import dataclass
from enum import StrEnum, auto
class PlayerRole(StrEnum):
SETTER = auto()
GUESSER = auto()
@dataclass(frozen=True)
class Player:
name: str
role: PlayerRole
@classmethod
def from_dict(cls, d: dict) -> "Player":
return cls(
name=d["player"]["name"], role=PlayerRole(d["player"]["role"])
)
@@ -0,0 +1,27 @@
from enum import Enum, StrEnum, auto
class ArrowDirection(StrEnum):
LEFT = auto()
RIGHT = auto()
HIDDEN = auto()
class SessionPhase(Enum):
INITIALIZING = auto()
WAITING_FOR_SETTER = auto()
WAITING_FOR_GUESSER = auto()
WAITING_FOR_REVEAL = auto()
CHECKOUT = auto()
FINISHED = auto()
class CardState(Enum):
SETTER_NOT_SET = auto()
SETTER_WAITING = auto()
SETTER_HIDDEN = auto()
SETTER_REVEALED = auto()
GUESSER_DISABLED = auto()
GUESSER_WAITING = auto()
GUESSER_CORRECT = auto()
GUESSER_WRONG = auto()
@@ -0,0 +1,71 @@
from dataclasses import dataclass
from ..base_event import BaseEvent
from ..lobby.domain import Player
from ..session.domain import ArrowDirection
@dataclass(frozen=True)
class PlayerHelloEvent(BaseEvent):
player: Player
@classmethod
def from_payload(cls, p: dict) -> "PlayerHelloEvent":
return cls(player=Player.from_dict(p))
@dataclass(frozen=True)
class SetterMoveEvent(BaseEvent):
player: Player
direction: ArrowDirection
@classmethod
def from_payload(cls, p: dict) -> "SetterMoveEvent":
return cls(
player=Player.from_dict(p),
direction=ArrowDirection(p["arrow_direction"]),
)
@dataclass(frozen=True)
class GuesserMoveEvent(BaseEvent):
player: Player
direction: ArrowDirection
@classmethod
def from_payload(cls, p: dict) -> "GuesserMoveEvent":
return cls(
player=Player.from_dict(p),
direction=ArrowDirection(p["arrow_direction"]),
)
@dataclass(frozen=True)
class RevealSecretEvent(BaseEvent):
player: Player
direction: ArrowDirection
@classmethod
def from_payload(cls, p: dict) -> "RevealSecretEvent":
return cls(
player=Player.from_dict(p),
direction=ArrowDirection(p["arrow_direction"]),
)
@dataclass(frozen=True)
class NextStepRequestedEvent(BaseEvent):
player: Player
@classmethod
def from_payload(cls, p: dict) -> "NextStepRequestedEvent":
return cls(player=Player.from_dict(p))
SessionEvent = (
PlayerHelloEvent
| SetterMoveEvent
| GuesserMoveEvent
| RevealSecretEvent
| NextStepRequestedEvent
)
@@ -0,0 +1,174 @@
from ..lobby.domain import Player, PlayerRole
from ..session.domain import ArrowDirection, CardState, SessionPhase
from ..session.events import (
GuesserMoveEvent,
NextStepRequestedEvent,
PlayerHelloEvent,
RevealSecretEvent,
SessionEvent,
SetterMoveEvent,
)
class SessionStateMachine:
def __init__(
self, setter: Player, guessers: set[Player], sequence_length: int = 20
):
self._setter = setter
self._guessers = guessers
self._pending_players: set[Player] = set(guessers)
self._pending_players.add(setter)
self._sequence_length = sequence_length
self._setter_moves: list[ArrowDirection] = []
self._guessers_moves: dict[Player, list[ArrowDirection]] = {
player: [] for player in guessers
}
self._phase: SessionPhase = SessionPhase.INITIALIZING
def process(self, event: SessionEvent) -> bool:
match (self._phase, event):
case (SessionPhase.INITIALIZING, PlayerHelloEvent(player)):
self._pending_players.remove(player)
if not self._pending_players:
self._phase = SessionPhase.WAITING_FOR_SETTER
self._pending_players = {self._setter}
return True
case (
SessionPhase.WAITING_FOR_SETTER,
SetterMoveEvent(player, direction),
):
self._pending_players.remove(player)
if not self._pending_players:
self._setter_moves.append(direction)
self._phase = SessionPhase.WAITING_FOR_GUESSER
self._pending_players = set(self._guessers)
return True
case (
SessionPhase.WAITING_FOR_GUESSER,
GuesserMoveEvent(player, direction),
):
self._guessers_moves[player].append(direction)
self._pending_players.remove(player)
if not self._pending_players:
self._phase = SessionPhase.WAITING_FOR_REVEAL
self._pending_players = {self._setter}
return True
case (
SessionPhase.WAITING_FOR_REVEAL,
RevealSecretEvent(player, direction),
):
self._pending_players.remove(player)
if not self._pending_players:
self._phase = SessionPhase.CHECKOUT
setter_move_index = len(self._setter_moves) - 1
self._setter_moves[setter_move_index] = direction
self._pending_players = set(self._guessers)
return True
case (SessionPhase.CHECKOUT, NextStepRequestedEvent(player)):
self._pending_players.remove(player)
if not self._pending_players:
if len(self._setter_moves) >= self._sequence_length:
self._phase = SessionPhase.FINISHED
self._pending_players.clear()
else:
self._phase = SessionPhase.WAITING_FOR_SETTER
self._pending_players = {self._setter}
return True
case _:
return False
def get_card_state(self, player: Player, index: int) -> CardState:
match player.role:
case PlayerRole.SETTER:
return self._get_setter_card_state(index)
case PlayerRole.GUESSER:
return self._get_guesser_card_state(player, index)
def _get_setter_card_state(self, index: int) -> CardState:
current_index = self.current_index
if index > current_index:
return CardState.SETTER_NOT_SET
elif index == current_index:
match self._phase:
case SessionPhase.INITIALIZING:
return CardState.SETTER_NOT_SET
case SessionPhase.WAITING_FOR_SETTER:
return CardState.SETTER_WAITING
case (
SessionPhase.WAITING_FOR_GUESSER
| SessionPhase.WAITING_FOR_REVEAL
):
if self._setter_moves[-1] == ArrowDirection.HIDDEN:
return CardState.SETTER_HIDDEN
else:
return CardState.SETTER_REVEALED
case SessionPhase.CHECKOUT | SessionPhase.FINISHED:
return CardState.SETTER_REVEALED
else:
return CardState.SETTER_REVEALED
def _get_guesser_card_state(self, player: Player, index: int) -> CardState:
def _check_direction(
self: SessionStateMachine, player: Player, index: int
) -> CardState:
if self._setter_moves[index] == self._guessers_moves[player][index]:
return CardState.GUESSER_CORRECT
else:
return CardState.GUESSER_WRONG
current_index = self.current_index
if index > current_index:
return CardState.GUESSER_DISABLED
elif index == current_index:
match self._phase:
case (
SessionPhase.INITIALIZING | SessionPhase.WAITING_FOR_SETTER
):
return CardState.GUESSER_DISABLED
case (
SessionPhase.WAITING_FOR_GUESSER
| SessionPhase.WAITING_FOR_REVEAL
):
return CardState.GUESSER_WAITING
case SessionPhase.CHECKOUT | SessionPhase.FINISHED:
return _check_direction(self, player, index)
else:
return _check_direction(self, player, index)
def get_setter_move(self, index: int) -> ArrowDirection | None:
try:
return self._setter_moves[index]
except IndexError:
return None
def get_guesser_move(
self, player: Player, index: int
) -> ArrowDirection | None:
try:
return self._guessers_moves[player][index]
except IndexError:
return None
@property
def current_index(self) -> int:
setter_step_number = len(self._setter_moves)
if setter_step_number:
if self._phase == SessionPhase.WAITING_FOR_SETTER:
return setter_step_number
else:
return setter_step_number - 1
return 0
+5 -1
View File
@@ -29,6 +29,7 @@ class GuesserCardColour:
class ArrowSymbol: class ArrowSymbol:
LEFT: str = "" LEFT: str = ""
RIGHT: str = "" RIGHT: str = ""
HIDDEN: str = "?"
class Card(wx.Panel): class Card(wx.Panel):
@@ -61,7 +62,7 @@ class Card(wx.Panel):
case CardState.SETTER_HIDDEN: case CardState.SETTER_HIDDEN:
colour = SetterCardColour.HIDDEN colour = SetterCardColour.HIDDEN
show_arrow = False show_arrow = True
case CardState.SETTER_REVEALED: case CardState.SETTER_REVEALED:
colour = SetterCardColour.REVEALED colour = SetterCardColour.REVEALED
@@ -91,6 +92,9 @@ class Card(wx.Panel):
case ArrowDirection.RIGHT: case ArrowDirection.RIGHT:
symbol = ArrowSymbol.RIGHT symbol = ArrowSymbol.RIGHT
case ArrowDirection.HIDDEN:
symbol = ArrowSymbol.HIDDEN
self.label.SetLabel(symbol) self.label.SetLabel(symbol)
else: else: