12 Commits
17 changed files with 849 additions and 191 deletions
+3
View File
@@ -39,3 +39,6 @@ venv/
ENV/
env.bak/
venv.bak/
# Sandbox
sandbox
+22
View File
@@ -0,0 +1,22 @@
from .common import Player, PlayerRole
from .context import SessionContext
from .session import (
ArrowDirection,
CardData,
CardState,
SessionStats,
StepData,
StepState,
)
__all__ = [
"Player",
"PlayerRole",
"SessionContext",
"ArrowDirection",
"CardData",
"CardState",
"SessionStats",
"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
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"
+54
View File
@@ -0,0 +1,54 @@
from dataclasses import dataclass
from enum import Enum, StrEnum, auto
from typing import Optional
class ArrowDirection(StrEnum):
LEFT = auto()
RIGHT = auto()
class StepState(Enum):
WAITING_FOR_SETTER = auto()
WAITING_FOR_GUESSER = auto()
SESSION_FINISHED = auto()
class CardState(Enum):
SETTER_NOT_SET = 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)
+3
View File
@@ -0,0 +1,3 @@
from .session_input import SessionInput
__all__ = ["SessionInput"]
+28
View File
@@ -0,0 +1,28 @@
from ..domain import ArrowDirection, Player
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:
payload = {
"player": {
"name": self._local_player.name,
"role": self._local_player.role,
},
"arrow_direction": direction,
}
self._network.send(
NetworkMessage(NetworkMessageKind.SESSION_PLAYER_MOVED, payload)
)
def finish_session(self) -> None:
pass
+6 -2
View File
@@ -1,7 +1,11 @@
from .controller import NetworkController
from .protocol import NetworkMesssage
from .enums import NetworkMessageKind
from .handlers import SessionNetworkMessageHandler
from .message import NetworkMessage
__all__ = [
"NetworkController",
"NetworkMesssage",
"NetworkMessageKind",
"SessionNetworkMessageHandler",
"NetworkMessage",
]
+46 -138
View File
@@ -1,150 +1,58 @@
import socket
import struct
import threading
from typing import Callable, Optional
from typing import Callable
import wx
import paho.mqtt.client as mqtt
from paho.mqtt.enums import CallbackAPIVersion
from .protocol import NetworkMesssage
from .enums import NetworkMessageKind
from .message import NetworkMessage
OnMessageCallback = Callable[[NetworkMesssage], None]
OnNoticeCallback = Callable[[str], None]
MessageHandler = Callable[[NetworkMessage], None]
def receive_exact(sock: socket.socket, n: int) -> Optional[bytes]:
data = bytearray()
class NetworkController:
def __init__(self, host: str, port: int, username: str, password: str):
self._handlers: set[MessageHandler] = set()
while len(data) < n:
packet = sock.recv(n - len(data))
if not packet:
return None
data.extend(packet)
self._topic = "mind_reader"
self._client = mqtt.Client(CallbackAPIVersion.VERSION2)
self._client.username_pw_set(username, password)
self._client.on_message = self._on_message
self._client.connect(host, port)
self._client.subscribe("mind_reader/#")
return bytes(data)
class NetworkController(threading.Thread):
def __init__(
self,
on_message_callback: OnMessageCallback,
on_notice_callback: Optional[OnNoticeCallback] = None,
):
super().__init__(daemon=True)
self.on_message_callback = on_message_callback
self.on_notice_callback = on_notice_callback
self.sock: Optional[socket.socket] = None
self.conn: Optional[socket.socket] = None
self.is_running: bool = False
self.is_server: bool = False
def serve(self, host: str = "0.0.0.0", port: int = 8994) -> None:
self.is_server = True
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind((host, port))
self.sock.listen(1)
self.is_running = True
self.start()
def connect(self, host: str, port: int = 8994) -> bool:
self.is_server = False
try:
self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
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 start(self) -> None:
self._client.loop_start()
def stop(self) -> None:
self.is_running = False
self._client.loop_stop()
self._client.disconnect()
self._close_active_connection()
def add_handler(self, handler: MessageHandler) -> None:
self._handlers.add(handler)
if self.sock:
try:
self.sock.close()
except Exception:
pass
finally:
self.sock = None
def remove_handler(self, handler: MessageHandler) -> None:
self._handlers.remove(handler)
self._notify("Connection closed")
def send(self, network_message: NetworkMessage) -> None:
kind = network_message.kind
payload = network_message.to_json()
match kind:
case (
NetworkMessageKind.SESSION_PLAYER_MOVED
| NetworkMessageKind.SESSION_SESSION_FINISHED
| NetworkMessageKind.SESSION_SESSION_STARTED
):
topic = f"{self._topic}/session"
self._client.publish(topic, payload)
def _handle(self, network_message: NetworkMessage) -> None:
for handler in self._handlers:
handler(network_message)
def _on_message(
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)
+13
View File
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,27 @@
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
+34
View File
@@ -0,0 +1,34 @@
import json
from dataclasses import asdict, dataclass, field
from typing import Any, Optional
from uuid_extensions import uuid7str
from .enums import NetworkMessageKind
@dataclass(frozen=True)
class NetworkMessage:
kind: NetworkMessageKind
payload: dict[str, Any]
id: str = field(default_factory=uuid7str)
def to_json(self) -> str:
data = asdict(self)
data["kind"] = self.kind.value
return json.dumps(data)
@classmethod
def from_json(cls, json_str: str) -> "NetworkMessage":
try:
data = json.loads(json_str)
return cls(
id=data["id"],
kind=NetworkMessageKind(data["kind"]),
payload=data.get("payload", {}),
)
except (json.JSONDecodeError, KeyError, ValueError) as e:
return cls(
kind=NetworkMessageKind.SYSTEM_INC_MSG_PA_ERR,
payload={"exception": e},
)
-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[str, str] = 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"]
+202
View File
@@ -0,0 +1,202 @@
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:
current_index = len(self._setter_moves)
if index > current_index:
return CardState.SETTER_NOT_SET
elif index == current_index:
if self._step_state == StepState.WAITING_FOR_GUESSER:
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_move_count = self._completed_move_count
if index > completed_move_count:
return CardState.GUESSER_DISABLED
elif index == completed_move_count:
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)
if self._completed_move_count < self._sequence_length:
self._step_state = StepState.WAITING_FOR_SETTER
else:
self._step_state = StepState.SESSION_FINISHED
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 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.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
+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__ = [
"LobbyPanel",
]
__all__ = ["SessionPanel"]
+375
View File
@@ -0,0 +1,375 @@
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 = ""
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 = False
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
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])