26 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
vladimir b78081d999 feat: почти рабочий вариант session 2026-08-18 03:53:34 +03:00
vladimir a7fcf3dda8 refactor: разделение domain.py на модуль domain, включающий common.py и session.py 2026-08-17 02:03:46 +03:00
vladimir ab6fd43883 feat: начало работы на AppContext 2026-08-14 03:17:30 +03:00
vladimir dc25e2d98b feat: начало работы над SessionNetworkController 2026-08-14 03:17:08 +03:00
vladimir ac7a841d05 feat: начало работы над SessionInput 2026-08-14 03:16:24 +03:00
vladimir bc5ae5394c chore: добавление директории sandbox в .gitignore 2026-08-14 02:43:59 +03:00
vladimir e3f5828334 refactor: разделение кода в session_view.py 2026-08-14 02:42:18 +03:00
vladimir c657415bf0 feat: правильная работа SessionState 2026-08-13 03:41:03 +03:00
vladimir 6a66a02071 feat: wip SessionState, SessionView 2026-08-12 04:30:17 +03:00
vladimir da169b49fc refactor(ui): wip session_view.py 2026-08-12 01:21:54 +03:00
vladimir 6254de74e4 feat(ui): wip session_view.py 2026-08-11 04:08:55 +03:00
vladimir 1da622cd7c feat(ui): wip session_view.py 2026-08-10 03:29:23 +03:00
vladimir 63a51c7ae3 feat(ui): работа с новыми классами состояний приложения 2026-08-09 02:10:02 +03:00
vladimir c07bf63b8b fix(net): аннотация payload: dict[str, str] в классе NetwrokMessage 2026-08-09 02:09:01 +03:00
vladimir f80a9b4098 feat(core): отдельный класс для каждого состояния приложения 2026-08-09 02:05:53 +03:00
28 changed files with 1598 additions and 282 deletions
+3
View File
@@ -39,3 +39,6 @@ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Sandbox
sandbox
+4 -2
View File
@@ -1,5 +1,7 @@
from mind_reader.core.game_state import GameState from .state import AppState, LobbyState, SessionState
__all__ = [ __all__ = [
"GameState", "AppState",
"LobbyState",
"SessionState",
] ]
-33
View File
@@ -1,33 +0,0 @@
class GameState:
def __init__(self, local_player_name: str):
self.local_player_name: str = local_player_name
self.remote_player_name: str | None = None
self.lobby_players: dict[str, str] = {self.local_player_name: "NONE"}
def add_remote_player(self, player_name: str):
self.remote_player_name = player_name
self.lobby_players[self.remote_player_name] = "NONE"
def delete_remote_player(self):
if self.remote_player_name in self.lobby_players:
del self.lobby_players[self.remote_player_name]
self.remote_player_name = None
def set_player_role(self, player_name: str, role: str) -> bool:
current_role = self.lobby_players.get(player_name)
if current_role == role:
return False
if role in ["NONE", "SETTER", "GUESSER"]:
self.lobby_players[player_name] = role
return True
return False
@property
def local_player_role(self) -> str:
return self.lobby_players[self.local_player_name]
def is_session_ready(self) -> bool:
roles = list(self.lobby_players.values())
return ("SETTER" in roles) and ("GUESSER" in roles)
+172
View File
@@ -0,0 +1,172 @@
from typing import Callable, Optional
from wx.lib.pubsub import pub
from ..network import NetworkController, NetworkMesssage
OnNetworkEventCallback = Callable[[dict[str, str]], None]
class BaseState:
def __init__(self, app_state: "AppState"):
self.app_state = app_state
def handle_network_message(self, message: NetworkMesssage) -> None:
pass
class LobbyState(BaseState):
def __init__(self, app_state: "AppState", local_player_name: str):
super().__init__(app_state)
self.local_player_name: str = local_player_name
self.remote_player_name: Optional[str] = None
self.players: dict[str, str] = {self.local_player_name: "NONE"}
def handle_network_message(self, message: NetworkMesssage) -> None:
callbacks: dict[str, OnNetworkEventCallback] = {
"add_player": self.handle_add_player,
"get_player": self.handle_get_player,
"set_role": self.handle_set_player_role,
"disconnect": self.handle_disconnect,
"server_stop": self.handle_server_stop,
}
callback = callbacks.get(message.event, None)
if callback:
callback(message.payload)
pub.sendMessage("lobby.refresh", state=self)
def send_local_player(self) -> None:
player_name = self.local_player_name
player_role = self.players.get(player_name, "NONE")
self.app_state.network.send(
"add_player",
{
"player_name": player_name,
"player_role": player_role,
},
)
def get_remote_player(self) -> None:
self.app_state.network.send("get_player")
def set_player_role(self, role: str) -> bool:
current_role = self.players[self.local_player_name]
result = False
if current_role == role:
result = False
if role in ["NONE", "SETTER", "GUESSER"]:
self.players[self.local_player_name] = role
result = True
if result:
if self.app_state.network.conn:
self.app_state.network.send(
"set_role",
{
"player_name": self.local_player_name,
"player_role": role,
},
)
return result
def send_client_disconnect(self) -> None:
self.app_state.network.send(
"disconnect", {"player_name": self.local_player_name}
)
def send_server_stop(self) -> None:
self.app_state.network.send("server_stop")
def handle_add_player(self, payload: dict[str, str]) -> None:
player_name = payload["player_name"]
player_role = payload["player_role"]
self.remote_player_name = player_name
self.players[self.remote_player_name] = player_role
def handle_get_player(self, payload: dict[str, str]) -> None:
self.send_local_player()
def handle_set_player_role(self, payload: dict[str, str]) -> None:
player_name = payload["player_name"]
player_role = payload["player_role"]
self.players[player_name] = player_role
def handle_disconnect(self, payload: dict[str, str]) -> None:
player_name = payload["player_name"]
self.players.pop(player_name)
self.remote_player_name = None
def handle_server_stop(self, payload: dict[str, str]) -> None:
pub.sendMessage("mainframe.remote_server_stop", state=self.app_state)
@property
def local_player_role(self) -> str:
return self.players[self.local_player_name]
def is_ready(self) -> bool:
roles = list(self.players.values())
return ("SETTER" in roles) and ("GUESSER" in roles)
class SessionState(BaseState):
def __init__(self, app_state: "AppState"):
super().__init__(app_state)
class AppState:
def __init__(self):
self.network = NetworkController(
self.handle_network_message,
self.handle_network_notice,
)
self.lobby: Optional[LobbyState] = None
self.session: Optional[SessionState] = None
def handle_network_message(self, message: NetworkMesssage) -> None:
if self.lobby:
self.lobby.handle_network_message(message)
if self.session:
self.session.handle_network_message(message)
def handle_network_notice(self, notice: str) -> None:
pub.sendMessage("mainframe.statusbar", notice=notice)
def recreate_network_controller(self) -> None:
self.network.stop()
self.network = NetworkController(
self.handle_network_message,
self.handle_network_notice,
)
def connect(self, player_name: str, host: str, port: int = 8994) -> bool:
if self.network.connect(host, port):
self.lobby = LobbyState(self, player_name)
self.lobby.send_local_player()
self.lobby.get_remote_player()
return True
else:
self.recreate_network_controller()
return False
def serve(self, player_name: str, host: str, port: int = 8994) -> None:
self.network.serve(host, port)
self.lobby = LobbyState(self, player_name)
def disconnect(self) -> None:
if self.lobby:
if self.network.is_server:
self.lobby.send_server_stop()
else:
self.lobby.send_client_disconnect()
self.lobby = None
self.recreate_network_controller()
+36
View File
@@ -0,0 +1,36 @@
from .common import Player, PlayerRole
from .context import SessionContext
from .session import (
ArrowDirection,
CardData,
CardState,
GuesserMoveEvent,
NextStepRequestedEvent,
PlayerHelloEvent,
RevealSecretEvent,
SessionEvent,
SessionPhase,
SessionStats,
SetterMoveEvent,
StepData,
StepState,
)
__all__ = [
"Player",
"PlayerRole",
"SessionContext",
"ArrowDirection",
"CardData",
"CardState",
"GuesserMoveEvent",
"NextStepRequestedEvent",
"PlayerHelloEvent",
"RevealSecretEvent",
"SessionEvent",
"SessionPhase",
"SessionStats",
"SetterMoveEvent",
"StepData",
"StepState",
]
+13
View File
@@ -0,0 +1,13 @@
from dataclasses import dataclass
from enum import Enum, StrEnum, auto
class PlayerRole(StrEnum):
SETTER = auto()
GUESSER = auto()
@dataclass(frozen=True)
class Player:
name: str
role: PlayerRole
+16
View File
@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..input import SessionInput
from ..network import NetworkController
from ..state import SessionState
from ..ui.views import SessionPanel
@dataclass(frozen=True)
class SessionContext:
input: "SessionInput"
network: "NetworkController"
state: "SessionState"
view: "SessionPanel"
+105
View File
@@ -0,0 +1,105 @@
from dataclasses import dataclass
from enum import Enum, StrEnum, auto
from typing import Optional
from .common import Player
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()
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()
@dataclass(frozen=True)
class CardData:
direction: Optional[ArrowDirection]
state: CardState
@dataclass(frozen=True)
class StepData:
index: int
setter: CardData
guesser: CardData
@dataclass
class SessionStats:
total: int
setted: int
correct: int
wrong: int
taken_time: float
@property
def accuracy(self) -> float:
accuracy: float
if self.total == 0:
accuracy = 0.0
accuracy = (self.correct / self.total) * 100
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
)
+3
View File
@@ -0,0 +1,3 @@
from .session_input import SessionInput
__all__ = ["SessionInput"]
+70
View File
@@ -0,0 +1,70 @@
from ..domain import ArrowDirection, Player, PlayerRole
from ..network import NetworkController, NetworkMessage, NetworkMessageKind
class SessionInput:
def __init__(self, local_player: Player, network: NetworkController):
self._local_player = local_player
self._network = network
def start_session(self) -> None:
self._network.send(
NetworkMessage(NetworkMessageKind.SESSION_SESSION_STARTED, {})
)
def player_move(self, direction: ArrowDirection) -> None:
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
+10 -6
View File
@@ -1,7 +1,11 @@
from .controller import NetworkController # from .controller import NetworkController
from .protocol import NetworkMesssage # from .enums import NetworkMessageKind
# from .handlers import SessionNetworkMessageHandler
# from .message import NetworkMessage
__all__ = [ # __all__ = [
"NetworkController", # "NetworkController",
"NetworkMesssage", # "NetworkMessageKind",
] # "SessionNetworkMessageHandler",
# "NetworkMessage",
# ]
+89 -129
View File
@@ -1,150 +1,110 @@
import socket import logging
import struct from typing import Optional
import threading
from typing import Callable, Optional
import wx import paho.mqtt.client as mqtt
from paho.mqtt.enums import CallbackAPIVersion
from .protocol import NetworkMesssage from ..state_machines.session.events import SessionEvent
from ..state_machines.session.machine import SessionStateMachine
from .message import EventMapper, NetworkMessage, SessionMessageKind
OnMessageCallback = Callable[[NetworkMesssage], None] logger = logging.getLogger(__name__)
OnNoticeCallback = Callable[[str], None]
def receive_exact(sock: socket.socket, n: int) -> Optional[bytes]: class NetworkController:
data = bytearray()
while len(data) < n:
packet = sock.recv(n - len(data))
if not packet:
return None
data.extend(packet)
return bytes(data)
class NetworkController(threading.Thread):
def __init__( def __init__(
self, self,
on_message_callback: OnMessageCallback, host: str,
on_notice_callback: Optional[OnNoticeCallback] = None, port: int,
username: str,
password: str,
lobby,
session: SessionStateMachine,
): ):
super().__init__(daemon=True) self._lobby_topic = "mind_reader/lobby"
self.on_message_callback = on_message_callback self._session_topic = "mind_reader/session"
self.on_notice_callback = on_notice_callback
self.sock: Optional[socket.socket] = None self._lobby = lobby
self.conn: Optional[socket.socket] = None self._session = session
self.is_running: bool = False self._pending_loopbacks: dict[str, NetworkMessage] = {}
self.is_server: bool = False
def serve(self, host: str = "0.0.0.0", port: int = 8994) -> None: self._client = mqtt.Client(CallbackAPIVersion.VERSION2)
self.is_server = True self._client.username_pw_set(username, password)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._client.message_callback_add(
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._lobby_topic, self._on_lobby_message
self.sock.bind((host, port)) )
self.sock.listen(1) self._client.message_callback_add(
self._session_topic, self._on_session_message
)
self.is_running = True self._client.connect(host, port)
self.start()
def connect(self, host: str, port: int = 8994) -> bool: self._client.subscribe(self._lobby_topic)
self.is_server = False self._client.subscribe(self._session_topic)
try: def start(self) -> None:
self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._client.loop_start()
self.conn.connect((host, port))
self.is_running = True
self._notify("Connected to host")
self.start()
return True
except Exception as e:
self._notify(f"Connection failed: {e}")
return False
def send(self, event: str, payload: dict = {}) -> None:
if not self.conn or not self.is_running:
return
m = NetworkMesssage(event, payload)
try:
self.conn.sendall(m.encode())
except Exception as e:
self._notify(f"Send error: {e}")
self._close_active_connection()
def run(self) -> None:
HEADER_SIZE = 4
while self.is_running:
if self.is_server and self.sock and not self.conn:
self._notify("Waiting for peer to connect...")
try:
self.conn, addr = self.sock.accept()
self._notify(f"Peer connected from {addr[0]}:{addr[1]}")
except Exception:
break
while self.is_running and self.conn:
try:
header_bytes = receive_exact(self.conn, HEADER_SIZE)
if not header_bytes:
self._notify("Peer disconnected")
break
body_size = struct.unpack(">I", header_bytes)[0]
body_bytes = receive_exact(self.conn, body_size)
if not body_bytes:
self._notify("Peer disconnected unexpectedly")
break
json_str = body_bytes.decode("utf-8")
m = NetworkMesssage.from_json(json_str)
wx.CallAfter(self.on_message_callback, m)
except Exception as e:
self._notify(f"Read error: {e}")
break
self._close_active_connection()
if not self.is_server:
break
self.stop()
def _close_active_connection(self) -> None:
if self.conn:
try:
self.conn.shutdown(socket.SHUT_RDWR)
self.conn.close()
except Exception:
pass
finally:
self.conn = None
def _notify(self, notice: str) -> None:
if self.on_notice_callback:
wx.CallAfter(self.on_notice_callback, notice)
def stop(self) -> None: def stop(self) -> None:
self.is_running = False self._client.loop_stop()
self._client.disconnect()
self._close_active_connection() def send(self, network_message: NetworkMessage) -> None:
match network_message.kind:
case SessionMessageKind():
self._client.publish(
self._session_topic, network_message.to_json()
)
if self.sock: def send_with_loopback(
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}'"
f"vs network_message.id='{network_message.id}'"
)
self._pending_loopbacks[local_message.id] = local_message
self.send(network_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: try:
self.sock.close() json_str = message.payload.decode()
except Exception: network_message = NetworkMessage.from_json(json_str)
pass except Exception as err:
finally: logger.error("Failed to parse incoming MQTT message: %s", err)
self.sock = None return None
self._notify("Connection closed") 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
) -> None:
network_message = self._parse_incoming_message(message)
if network_message is None:
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)
+108
View File
@@ -0,0 +1,108 @@
import json
import logging
from dataclasses import asdict, dataclass, field
from enum import StrEnum, auto
from typing import Any, Optional
from uuid_extensions import uuid7str
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)
class NetworkMessage:
kind: NetworkMessageKind
payload: dict[str, Any] = field(default_factory=dict)
id: str = field(default_factory=uuid7str)
@classmethod
def from_json(cls, json_str: str) -> "NetworkMessage":
try:
data = json.loads(json_str)
return cls(
kind=NetworkMessageKind(data["kind"]),
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:
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
-46
View File
@@ -1,46 +0,0 @@
import itertools
import json
import struct
from dataclasses import dataclass, field
_id_generator = itertools.count(0)
@dataclass
class NetworkMesssage:
event: str
payload: dict = field(default_factory=dict)
id: int = field(default_factory=lambda: next(_id_generator))
version: str = "1.0"
def to_json(self) -> str:
return json.dumps(
{
"version": self.version,
"id": self.id,
"event": self.event,
"payload": self.payload,
},
ensure_ascii=False,
)
@classmethod
def from_json(cls, json_str: str) -> "NetworkMesssage":
data = json.loads(json_str)
return cls(
event=data.get("event", "unknown"),
payload=data.get("payload", {}),
id=data.get("id", -1),
version=data.get("version", "unknown"),
)
def encode(self) -> bytes:
raw_bytes = self.to_json().encode("utf-8")
length_prefix = struct.pack(">I", len(raw_bytes))
return length_prefix + raw_bytes
if __name__ == "__main__":
for i in range(10):
message = NetworkMesssage("none")
print(message.encode())
+3
View File
@@ -0,0 +1,3 @@
from .session_state import SessionState
__all__ = ["SessionState"]
+219
View File
@@ -0,0 +1,219 @@
from wx.lib.pubsub import pub
from mind_reader.domain import (
ArrowDirection,
CardData,
CardState,
Player,
PlayerRole,
StepData,
StepState,
)
class SessionState:
def __init__(
self,
local_player: Player,
remote_player: Player,
sequence_length: int = 20,
):
self._local_player = local_player
self._remote_player = remote_player
self._sequence_length = sequence_length
self._setter_moves: list[ArrowDirection] = []
self._guesser_moves: list[ArrowDirection] = []
self._step_state: StepState = StepState.WAITING_FOR_SETTER
@property
def _completed_move_count(self):
return len(self._guesser_moves)
def _get_setter_card_state(self, index: int) -> CardState:
completed = self._completed_move_count
if index > completed:
return CardState.SETTER_NOT_SET
elif index == completed:
if self._local_player.role == PlayerRole.SETTER:
return CardState.SETTER_REVEALED
else:
return CardState.SETTER_HIDDEN
return CardState.SETTER_REVEALED
def _get_guesser_card_state(self, index: int) -> CardState:
completed = self._completed_move_count
if index > completed:
return CardState.GUESSER_DISABLED
elif index == completed:
if self._step_state == StepState.WAITING_FOR_GUESSER:
return CardState.GUESSER_WAITING
else:
return CardState.GUESSER_DISABLED
else:
setter_move = self._setter_moves[index]
guesser_move = self._guesser_moves[index]
if setter_move == guesser_move:
return CardState.GUESSER_CORRECT
else:
return CardState.GUESSER_WRONG
def start_session(self) -> None:
index = 0
pub.sendMessage(
"session.session_started",
step=StepData(
index=index,
setter=CardData(
direction=None,
state=self._get_setter_card_state(index),
),
guesser=CardData(
direction=None,
state=self._get_guesser_card_state(index),
),
),
)
def setter_move(self, direction: ArrowDirection) -> bool:
if self._step_state != StepState.WAITING_FOR_SETTER:
return False
self._setter_moves.append(direction)
self._step_state = StepState.WAITING_FOR_GUESSER
index = len(self._setter_moves) - 1
pub.sendMessage(
"session.setter_moved",
step=StepData(
index=index,
setter=CardData(
direction=direction,
state=self._get_setter_card_state(index),
),
guesser=CardData(
direction=direction,
state=self._get_guesser_card_state(index),
),
),
)
return True
def guesser_move(self, direction: ArrowDirection) -> bool:
if self._step_state != StepState.WAITING_FOR_GUESSER:
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(
"session.guesser_moved",
step=StepData(
index=index,
setter=CardData(
direction=self._setter_moves[index],
state=self._get_setter_card_state(index),
),
guesser=CardData(
direction=self._guesser_moves[index],
state=self._get_guesser_card_state(index),
),
),
)
next = index + 1
pub.sendMessage(
"session.step_closed",
step=StepData(
index=next,
setter=CardData(
direction=None,
state=self._get_setter_card_state(next),
),
guesser=CardData(
direction=None,
state=self._get_guesser_card_state(next),
),
),
)
if self._step_state == StepState.SESSION_FINISHED:
pub.sendMessage("session.session_finished")
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
if self._local_player.role == PlayerRole.SETTER:
return self.setter_move(direction)
else:
return self.guesser_move(direction)
def can_local_move(self) -> bool:
match self._step_state:
case StepState.WAITING_FOR_SETTER:
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
@property
def setter_name(self) -> str:
if self._local_player.role == PlayerRole.SETTER:
return self._local_player.name
else:
return self._remote_player.name
@property
def guesser_name(self) -> str:
if self._local_player.role == PlayerRole.GUESSER:
return self._local_player.name
else:
return self._remote_player.name
@property
def sequence_length(self) -> int:
return self._sequence_length
@property
def local_player(self) -> Player:
return self._local_player
@property
def remote_player(self) -> Player:
return self._remote_player
@@ -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
+31 -47
View File
@@ -1,10 +1,12 @@
import wx from typing import Optional
from mind_reader.core import GameState import wx
from mind_reader.network import NetworkController, NetworkMesssage from wx.lib.pubsub import pub
from mind_reader.ui.dialogs import ClientConnectDialog, ServerStartDialog
from mind_reader.ui.menu_bar import AppMenuBar from ..core import AppState
from mind_reader.ui.views import LobbyPanel from ..ui.dialogs import ClientConnectDialog, ServerStartDialog
from ..ui.menu_bar import AppMenuBar
from ..ui.views import LobbyPanel
class MainFrame(wx.Frame): class MainFrame(wx.Frame):
@@ -16,12 +18,9 @@ class MainFrame(wx.Frame):
size=wx.Size(700, 500), size=wx.Size(700, 500),
style=wx.DEFAULT_FRAME_STYLE, style=wx.DEFAULT_FRAME_STYLE,
) )
self.state: GameState | None = None self.state: AppState = AppState()
self.network = NetworkController(
self._handle_network_message, self.lobby_panel: Optional[LobbyPanel] = None
self._handle_network_notice,
)
self.lobby_panel: LobbyPanel | None = None
self.Center() self.Center()
@@ -32,7 +31,8 @@ class MainFrame(wx.Frame):
self.SetMenuBar(self.menu_bar) self.SetMenuBar(self.menu_bar)
self.CreateStatusBar(1) self.CreateStatusBar(1)
# self.SetStatusText("Подключение отсутствует") pub.subscribe(self.set_statusbar_text, "mainframe.statusbar")
pub.subscribe(self.remote_server_stop, "mainframe.remote_server_stop")
self._bind_menu_events() self._bind_menu_events()
@@ -45,11 +45,17 @@ class MainFrame(wx.Frame):
wx.EVT_MENU, self.on_disconnect, self.menu_bar.item_disconnect wx.EVT_MENU, self.on_disconnect, self.menu_bar.item_disconnect
) )
def set_statusbar_text(self, notice: str):
self.SetStatusText(notice)
def remote_server_stop(self, state: AppState):
self.on_disconnect(None)
def _show_lobby(self): def _show_lobby(self):
self._destroy_lobby() self._destroy_lobby()
if self.state: if self.state.lobby:
self.lobby_panel = LobbyPanel(self, self.state) self.lobby_panel = LobbyPanel(self, self.state.lobby)
self.main_sizer.Add(self.lobby_panel, 1, wx.EXPAND) self.main_sizer.Add(self.lobby_panel, 1, wx.EXPAND)
self.Layout() self.Layout()
@@ -60,51 +66,29 @@ class MainFrame(wx.Frame):
self.lobby_panel = None self.lobby_panel = None
self.Layout() self.Layout()
def _handle_network_message(self, m: NetworkMesssage):
pass
def _handle_network_notice(self, notice: str):
self.SetStatusText(notice)
def _recreate_network_controller(self):
self.network.stop()
self.network = NetworkController(
self._handle_network_message,
self._handle_network_notice,
)
def on_client_connect(self, event): def on_client_connect(self, event):
d = ClientConnectDialog(self) d = ClientConnectDialog(self)
if d.ShowModal() == wx.ID_OK: if d.ShowModal() == wx.ID_OK:
name, ip = d.get_data() player_name, ip = d.get_data()
if self.network.connect(ip): if self.state.connect(player_name, ip):
self.state = GameState(name)
self._show_lobby()
self.menu_bar.set_connected_state(True) self.menu_bar.set_connected_state(True)
else: self._show_lobby()
self._recreate_network_controller()
# self.SetStatusText(f"Подключено к {ip} | Ваше имя {name}")
d.Destroy() d.Destroy()
def on_server_start(self, event): def on_server_start(self, event):
d = ServerStartDialog(self) d = ServerStartDialog(self)
if d.ShowModal() == wx.ID_OK:
name, ip = d.get_data()
self.network.serve(ip)
self.state = GameState(name)
if d.ShowModal() == wx.ID_OK:
player_name, ip = d.get_data()
self.state.serve(player_name, ip)
self.menu_bar.set_connected_state(True)
self._show_lobby() self._show_lobby()
self.menu_bar.set_connected_state(True)
# self.SetStatusText(f"Сервер запущен на {ip} | Ваше имя {name}")
d.Destroy() d.Destroy()
def on_disconnect(self, event): def on_disconnect(self, event):
self._recreate_network_controller()
self._destroy_lobby() self._destroy_lobby()
self.state.disconnect()
self.state = None
self.menu_bar.set_connected_state(False) self.menu_bar.set_connected_state(False)
# self.SetStatusText("Подключение отсутствует")
+3 -4
View File
@@ -1,5 +1,4 @@
from mind_reader.ui.views.lobby_view import LobbyPanel # from .lobby_view import LobbyPanel
from .session_view import SessionPanel
__all__ = [ __all__ = ["SessionPanel"]
"LobbyPanel",
]
+19 -13
View File
@@ -1,8 +1,9 @@
from typing import Callable, Optional, cast from typing import Callable, Optional
import wx import wx
from wx.lib.pubsub import pub
from mind_reader.core import GameState from ...core import LobbyState
class RoleColumn(wx.Panel): class RoleColumn(wx.Panel):
@@ -48,12 +49,13 @@ class RoleColumn(wx.Panel):
class LobbyPanel(wx.Panel): class LobbyPanel(wx.Panel):
def __init__(self, parent, state: GameState): def __init__(self, parent, state: LobbyState):
super().__init__(parent) super().__init__(parent)
self.state = state self.state = state
self._init_ui() self._init_ui()
self.Bind(wx.EVT_WINDOW_DESTROY, self._on_destroy)
def _init_ui(self): def _init_ui(self):
main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer = wx.BoxSizer(wx.VERTICAL)
@@ -83,12 +85,12 @@ class LobbyPanel(wx.Panel):
self.SetSizer(main_sizer) self.SetSizer(main_sizer)
self.refresh_from_state() pub.subscribe(self.refresh, "lobby.refresh")
self.refresh(self.state)
def _on_role_click(self, target_role: str): def _on_role_click(self, target_role: str):
success = self.state.set_player_role( success = self.state.set_player_role(target_role)
self.state.local_player_name, target_role
)
if not success: if not success:
wx.MessageBox( wx.MessageBox(
@@ -98,15 +100,19 @@ class LobbyPanel(wx.Panel):
) )
return return
self.refresh_from_state() self.refresh(self.state)
def _on_start_click(self, event): def _on_start_click(self, event):
wx.MessageBox("Сессия запущена!", "Старт", wx.OK | wx.ICON_INFORMATION) wx.MessageBox("Сессия запущена!", "Старт", wx.OK | wx.ICON_INFORMATION)
def refresh_from_state(self): def _on_destroy(self, event):
pub.unsubscribe(self.refresh, "lobby.refresh")
event.Skip()
def refresh(self, state: LobbyState):
players_by_role = {"NONE": [], "SETTER": [], "GUESSER": []} players_by_role = {"NONE": [], "SETTER": [], "GUESSER": []}
for player, role in self.state.lobby_players.items(): for player, role in state.players.items():
if role in players_by_role: if role in players_by_role:
players_by_role[role].append(player) players_by_role[role].append(player)
@@ -114,9 +120,9 @@ class LobbyPanel(wx.Panel):
self.column_setter.set_players(players_by_role["SETTER"]) self.column_setter.set_players(players_by_role["SETTER"])
self.column_guesser.set_players(players_by_role["GUESSER"]) self.column_guesser.set_players(players_by_role["GUESSER"])
local_player_role = self.state.local_player_role local_player_role = state.local_player_role
self.column_none.set_role_state(local_player_role) self.column_none.set_role_state(local_player_role)
self.column_setter.set_role_state(local_player_role) self.column_setter.set_role_state(local_player_role)
self.column_guesser.set_role_state(local_player_role) self.column_guesser.set_role_state(local_player_role)
self.button_start.Enable(self.state.is_session_ready()) self.button_start.Enable(state.is_ready())
+379
View File
@@ -0,0 +1,379 @@
import sys
from dataclasses import dataclass
from typing import Callable, Optional
import wx
from wx.lib.pubsub import pub
from ...domain import ArrowDirection, CardState, SessionStats, StepData
from ...input import SessionInput
from ...state import SessionState
@dataclass(frozen=True)
class SetterCardColour:
NOT_SET = wx.Colour(82, 97, 107)
HIDDEN = wx.Colour(201, 214, 223)
REVEALED = wx.Colour(240, 245, 249)
@dataclass(frozen=True)
class GuesserCardColour:
DISABLED = wx.Colour(82, 97, 107)
WAITING = wx.Colour(201, 214, 223)
CORRECT = wx.Colour(76, 175, 80)
WRONG = wx.Colour(229, 115, 115)
@dataclass(frozen=True)
class ArrowSymbol:
LEFT: str = ""
RIGHT: str = ""
HIDDEN: str = "?"
class Card(wx.Panel):
def __init__(self, parent, state: CardState):
super().__init__(parent, style=wx.BORDER_SIMPLE)
self._direction: Optional[ArrowDirection] = None
self._state = state
self._setup()
self._update()
def _setup(self):
self.SetMinSize(wx.Size(45, 75))
self.label = wx.StaticText(self, style=wx.ALIGN_CENTER)
font = self.label.GetFont()
font.SetPointSize(20)
self.label.SetFont(font)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.label, 0, wx.EXPAND)
self.SetSizer(sizer)
def _update(self):
match self._state:
case CardState.SETTER_NOT_SET:
colour = SetterCardColour.NOT_SET
show_arrow = False
case CardState.SETTER_HIDDEN:
colour = SetterCardColour.HIDDEN
show_arrow = True
case CardState.SETTER_REVEALED:
colour = SetterCardColour.REVEALED
show_arrow = True
case CardState.GUESSER_DISABLED:
colour = GuesserCardColour.DISABLED
show_arrow = False
case CardState.GUESSER_WAITING:
colour = GuesserCardColour.WAITING
show_arrow = False
case CardState.GUESSER_CORRECT:
colour = GuesserCardColour.CORRECT
show_arrow = True
case CardState.GUESSER_WRONG:
colour = GuesserCardColour.WRONG
show_arrow = True
if show_arrow and self._direction:
match self._direction:
case ArrowDirection.LEFT:
symbol = ArrowSymbol.LEFT
case ArrowDirection.RIGHT:
symbol = ArrowSymbol.RIGHT
case ArrowDirection.HIDDEN:
symbol = ArrowSymbol.HIDDEN
self.label.SetLabel(symbol)
else:
self.label.SetLabel("")
self.SetBackgroundColour(colour)
self.Layout()
self.Refresh()
def set(
self,
state: CardState,
direction: Optional[ArrowDirection] = None,
) -> None:
self._state = state
self._direction = direction
self._update()
class SessionPanel(wx.Panel):
def __init__(
self,
parent,
input: SessionInput,
setter_name: str,
guesser_name: str,
sequence_length: int,
):
super().__init__(parent)
self._input = input
self._setter_name = setter_name
self._guesser_name = guesser_name
self._sequence_length = sequence_length
self._setter_sequence: list[Card] = []
self._guesser_sequence: list[Card] = []
self._statusbar = self._get_statusbar()
self._setup_statusbar()
self._setup()
self._fit_parent_frame()
pub.subscribe(self._on_session_started, "session.session_started")
pub.subscribe(self._on_setter_moved, "session.setter_moved")
pub.subscribe(self._on_guesser_moved, "session.guesser_moved")
pub.subscribe(self._on_step_closed, "session.step_closed")
pub.subscribe(self._on_session_finished, "session.session_finished")
def _setup(self) -> None:
main_sizer = wx.BoxSizer(wx.VERTICAL)
setter_sizer = self._create_sequence_section(
title=f"Загадывает: {self._setter_name}",
sequence=self._setter_sequence,
sequence_length=self._sequence_length,
initial_card_state=CardState.SETTER_NOT_SET,
)
guesser_sizer = self._create_sequence_section(
title=f"Угадывает: {self._guesser_name}",
sequence=self._guesser_sequence,
sequence_length=self._sequence_length,
initial_card_state=CardState.GUESSER_DISABLED,
)
controls_sizer = self._create_controls_sizer(
on_left=self._on_left_click,
on_right=self._on_right_click,
)
secondary_sizer = wx.BoxSizer(wx.VERTICAL)
secondary_sizer.Add(setter_sizer, 0, wx.EXPAND)
secondary_sizer.Add(guesser_sizer, 0, wx.EXPAND)
secondary_sizer.Add(
controls_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 25
)
main_sizer.Add(secondary_sizer, 0, wx.LEFT | wx.ALL, 0)
self.SetSizer(main_sizer)
def _create_sequence_section(
self,
title: str,
sequence: list[Card],
sequence_length: int,
initial_card_state: CardState,
) -> wx.Sizer:
section_sizer = wx.BoxSizer(wx.VERTICAL)
title_label = wx.StaticText(self, label=title)
cards_sizer = wx.BoxSizer(wx.HORIZONTAL)
for _ in range(sequence_length):
card = Card(self, initial_card_state)
cards_sizer.Add(card, 0, wx.ALL, 4)
sequence.append(card)
section_sizer.Add(title_label, 0, wx.LEFT | wx.TOP, 9)
section_sizer.Add(cards_sizer, 0, wx.ALL, 5)
return section_sizer
def _create_controls_sizer(
self,
on_left: Callable[[wx.CommandEvent], None],
on_right: Callable[[wx.CommandEvent], None],
) -> wx.Sizer:
left = wx.Button(self, label=ArrowSymbol.LEFT, size=wx.Size(145, 55))
right = wx.Button(self, label=ArrowSymbol.RIGHT, size=wx.Size(145, 55))
font = left.GetFont()
font.SetPointSize(22)
left.SetFont(font)
right.SetFont(font)
left.Bind(wx.EVT_BUTTON, on_left)
right.Bind(wx.EVT_BUTTON, on_right)
controls_sizer = wx.BoxSizer(wx.HORIZONTAL)
controls_sizer.Add(left, 0, wx.RIGHT, 10)
controls_sizer.Add(right, 0, wx.LEFT, 10)
return controls_sizer
def _get_statusbar(self) -> Optional[wx.StatusBar]:
top_frame = self.GetTopLevelParent()
if isinstance(top_frame, wx.Frame):
statusbar = top_frame.GetStatusBar()
if not statusbar:
statusbar = top_frame.CreateStatusBar(1)
statusbar.SetFieldsCount(4)
return statusbar
return None
def _setup_statusbar(self) -> None:
if not self._statusbar:
return
message = " Ожидание начала игры... "
progress = f" Прогресс 0/{self._sequence_length} "
accuracy = " Точность 0.0% (0/0) "
taken_time = " Время 00:00 "
self._statusbar.SetStatusText(message, 0)
self._statusbar.SetStatusText(progress, 1)
self._statusbar.SetStatusText(accuracy, 2)
self._statusbar.SetStatusText(taken_time, 3)
self._autofit_statusbar([progress, accuracy, message])
def _autofit_statusbar(self, texts: list[str]) -> None:
if not self._statusbar:
return
dc = wx.ClientDC(self._statusbar)
width_1 = dc.GetTextExtent(texts[0]).width
width_2 = dc.GetTextExtent(texts[1]).width
width_3 = dc.GetTextExtent(texts[2]).width
self._statusbar.SetStatusWidths([-1, width_1, width_2, width_3])
def _fit_parent_frame(self) -> None:
top_frame = self.GetTopLevelParent()
if isinstance(top_frame, wx.Frame):
self.Layout()
best_size = self.GetBestSize()
min_size = top_frame.ClientToWindowSize(best_size)
top_frame.SetMinSize(min_size)
top_frame.Fit()
def _on_left_click(self, event: wx.CommandEvent) -> None:
self._handle_arrow_input(ArrowDirection.LEFT)
def _on_right_click(self, event: wx.CommandEvent) -> None:
self._handle_arrow_input(ArrowDirection.RIGHT)
def _handle_arrow_input(self, direction: ArrowDirection) -> None:
self._input.player_move(direction)
def _on_session_started(self, step: StepData) -> None:
self.update_setter_card(
step.index,
step.setter.state,
step.setter.direction,
)
self.update_guesser_card(
step.index,
step.guesser.state,
step.guesser.direction,
)
def _on_setter_moved(self, step: StepData) -> None:
self.update_setter_card(
step.index,
step.setter.state,
step.setter.direction,
)
self.update_guesser_card(
step.index,
step.guesser.state,
step.guesser.direction,
)
def _on_guesser_moved(self, step: StepData):
self.update_setter_card(
step.index,
step.setter.state,
step.setter.direction,
)
self.update_guesser_card(
step.index,
step.guesser.state,
step.guesser.direction,
)
def _on_step_closed(self, step: StepData):
self.update_setter_card(
step.index,
step.setter.state,
step.setter.direction,
)
self.update_guesser_card(
step.index,
step.guesser.state,
step.guesser.direction,
)
def _on_session_finished(self):
pass
def update_setter_card(
self,
index: int,
state: CardState,
direction: Optional[ArrowDirection] = None,
) -> None:
self._setter_sequence[index].set(state, direction)
def update_guesser_card(
self,
index: int,
state: CardState,
direction: Optional[ArrowDirection] = None,
) -> None:
self._guesser_sequence[index].set(state, direction)
def update_statusbar(
self,
message: Optional[str],
stats: SessionStats,
):
if not self._statusbar:
return
if message is not None:
self._statusbar.SetStatusText(message, 0)
progress = f" Прогресс {stats.setted}/{stats.total} "
self._statusbar.SetStatusText(progress, 1)
accuracy = (
f" Точность {stats.accuracy}% ({stats.correct}/{stats.wrong}) "
)
self._statusbar.SetStatusText(accuracy, 2)
minutes = int(stats.taken_time) // 60
seconds = int(stats.taken_time) % 60
taken_time = f" Время {minutes:02d}:{seconds:02d} "
self._statusbar.SetStatusText(taken_time, 3)
self._autofit_statusbar([progress, accuracy, taken_time])