Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4533789432 | ||
|
|
b1132715e1 | ||
|
|
878b83dc1c | ||
|
|
28030412d0 |
@@ -1,7 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from enum import StrEnum, auto
|
from enum import StrEnum, auto
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from uuid_extensions import uuid7str
|
from uuid_extensions import uuid7str
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ from ..state_machines.session.events import (
|
|||||||
SetterMoveEvent,
|
SetterMoveEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SessionMessageKind(StrEnum):
|
class SessionMessageKind(StrEnum):
|
||||||
PLAYER_HELLO = auto()
|
PLAYER_HELLO = auto()
|
||||||
@@ -42,6 +45,14 @@ KIND_TO_EVENT_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -50,17 +61,22 @@ class NetworkMessage:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, json_str: str) -> "NetworkMessage":
|
def from_json(cls, json_str: str) -> "NetworkMessage":
|
||||||
data = json.loads(json_str)
|
try:
|
||||||
return cls(
|
data = json.loads(json_str)
|
||||||
kind=NetworkMessageKind(data["kind"]),
|
return cls(
|
||||||
payload=data.get("payload", {}),
|
kind=NetworkMessageKind(data["kind"]),
|
||||||
id=data["id"],
|
payload=data.get("payload", {}),
|
||||||
)
|
id=data["id"],
|
||||||
|
)
|
||||||
|
except (KeyError, ValueError, json.JSONDecodeError) as err:
|
||||||
|
logger.error(
|
||||||
|
"Failed to deserialize NetworkMessage from JSON: %s", err
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
data = asdict(self)
|
data = {"kind": self.kind.value, "payload": self.payload, "id": self.id}
|
||||||
data["kind"] = self.kind.value
|
return json.dumps(data, ensure_ascii=False, default=serializer)
|
||||||
return json.dumps(data, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
class EventMapper:
|
class EventMapper:
|
||||||
@@ -70,9 +86,14 @@ class EventMapper:
|
|||||||
kind = EVENT_TO_KIND_MAP.get(event_type)
|
kind = EVENT_TO_KIND_MAP.get(event_type)
|
||||||
|
|
||||||
if not kind:
|
if not kind:
|
||||||
raise ValueError(f"Event {event_type} not registered")
|
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=asdict(event))
|
return NetworkMessage(kind=kind, payload=event.to_payload())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_message(message: NetworkMessage) -> Event | None:
|
def from_message(message: NetworkMessage) -> Event | None:
|
||||||
@@ -82,33 +103,6 @@ class EventMapper:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = message.payload
|
return event_cls.from_payload(message.payload)
|
||||||
|
|
||||||
player = Player(
|
|
||||||
name=payload["player"]["name"],
|
|
||||||
role=PlayerRole(payload["player"]["role"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
match message.kind:
|
|
||||||
case NetworkMessageKind.PLAYER_HELLO:
|
|
||||||
return PlayerHelloEvent(player=player)
|
|
||||||
case NetworkMessageKind.SETTER_MOVED:
|
|
||||||
return SetterMoveEvent(
|
|
||||||
player=player,
|
|
||||||
direction=ArrowDirection(payload["direction"]),
|
|
||||||
)
|
|
||||||
case NetworkMessageKind.GUESSER_MOVED:
|
|
||||||
return GuesserMoveEvent(
|
|
||||||
player=player,
|
|
||||||
direction=ArrowDirection(payload["direction"]),
|
|
||||||
)
|
|
||||||
case NetworkMessageKind.SECRET_REVEALED:
|
|
||||||
return RevealSecretEvent(
|
|
||||||
player=player,
|
|
||||||
direction=ArrowDirection(payload["direction"]),
|
|
||||||
)
|
|
||||||
case NetworkMessageKind.NEXT_STEP_REQUESTED:
|
|
||||||
return NextStepRequestedEvent(player=player)
|
|
||||||
|
|
||||||
except (KeyError, ValueError, TypeError):
|
except (KeyError, ValueError, TypeError):
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -11,3 +11,9 @@ class PlayerRole(StrEnum):
|
|||||||
class Player:
|
class Player:
|
||||||
name: str
|
name: str
|
||||||
role: PlayerRole
|
role: PlayerRole
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: dict) -> "Player":
|
||||||
|
return cls(
|
||||||
|
name=d["player"]["name"], role=PlayerRole(d["player"]["role"])
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,36 +1,66 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from ..base_event import BaseEvent
|
||||||
from ..lobby.domain import Player
|
from ..lobby.domain import Player
|
||||||
from ..session.domain import ArrowDirection
|
from ..session.domain import ArrowDirection
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PlayerHelloEvent:
|
class PlayerHelloEvent(BaseEvent):
|
||||||
player: Player
|
player: Player
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_payload(cls, p: dict) -> "PlayerHelloEvent":
|
||||||
|
return cls(player=Player.from_dict(p))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SetterMoveEvent:
|
class SetterMoveEvent(BaseEvent):
|
||||||
player: Player
|
player: Player
|
||||||
direction: ArrowDirection
|
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)
|
@dataclass(frozen=True)
|
||||||
class GuesserMoveEvent:
|
class GuesserMoveEvent(BaseEvent):
|
||||||
player: Player
|
player: Player
|
||||||
direction: ArrowDirection
|
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)
|
@dataclass(frozen=True)
|
||||||
class RevealSecretEvent:
|
class RevealSecretEvent(BaseEvent):
|
||||||
player: Player
|
player: Player
|
||||||
direction: ArrowDirection
|
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)
|
@dataclass(frozen=True)
|
||||||
class NextStepRequestedEvent:
|
class NextStepRequestedEvent(BaseEvent):
|
||||||
player: Player
|
player: Player
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_payload(cls, p: dict) -> "NextStepRequestedEvent":
|
||||||
|
return cls(player=Player.from_dict(p))
|
||||||
|
|
||||||
|
|
||||||
SessionEvent = (
|
SessionEvent = (
|
||||||
PlayerHelloEvent
|
PlayerHelloEvent
|
||||||
|
|||||||
Reference in New Issue
Block a user