Compare commits
4
Commits
dev2
...
6a66a02071
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a66a02071 | ||
|
|
da169b49fc | ||
|
|
6254de74e4 | ||
|
|
1da622cd7c |
@@ -0,0 +1,561 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import Callable, Optional
|
||||
|
||||
import wx
|
||||
from wx.lib.pubsub import pub
|
||||
|
||||
|
||||
def enable_high_dpi_support():
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
||||
except Exception:
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
ctypes.windll.user32.SetProcessDPIAware()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class ArrowDirection(Enum):
|
||||
LEFT = auto()
|
||||
RIGHT = auto()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
class StepState(Enum):
|
||||
WAITING_FOR_SETTER = auto()
|
||||
WAITING_FOR_GUESSER = auto()
|
||||
SESSION_FINISHED = auto()
|
||||
|
||||
|
||||
class PlayerRole(Enum):
|
||||
SETTER = auto()
|
||||
GUESSER = auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MoveResult:
|
||||
step_index: int
|
||||
setter_direction: Optional[ArrowDirection] = None
|
||||
setter_card_state: Optional[CardState] = None
|
||||
guesser_direction: Optional[ArrowDirection] = None
|
||||
guesser_card_state: Optional[CardState] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Player:
|
||||
name: str
|
||||
role: PlayerRole
|
||||
|
||||
|
||||
class SessionState:
|
||||
def __init__(
|
||||
self,
|
||||
local_player: Player,
|
||||
remote_player: Player,
|
||||
sequence_length: int = 20,
|
||||
):
|
||||
self._sequence_length = sequence_length
|
||||
|
||||
self._local_player = local_player
|
||||
self._remote_player = remote_player
|
||||
|
||||
self._completed_step_count: int = 0
|
||||
self._step_state: StepState = StepState.WAITING_FOR_SETTER
|
||||
|
||||
self._setter_moves: list[ArrowDirection] = []
|
||||
self._guesser_moves: list[ArrowDirection] = []
|
||||
|
||||
self._stats: Optional[SessionStats] = None
|
||||
|
||||
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
|
||||
|
||||
step_index = len(self._setter_moves) - 1
|
||||
result = MoveResult(
|
||||
step_index=step_index,
|
||||
setter_direction=direction,
|
||||
setter_card_state=self.get_setter_card_state(step_index),
|
||||
)
|
||||
|
||||
pub.sendMessage("session.setter_moved", result=result)
|
||||
|
||||
pub.sendMessage(
|
||||
"session.step_state_changed",
|
||||
step_state=self._step_state,
|
||||
)
|
||||
|
||||
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._completed_step_count += 1
|
||||
|
||||
step_index = len(self._guesser_moves) - 1
|
||||
|
||||
result = MoveResult(
|
||||
step_index=step_index,
|
||||
setter_direction=self._setter_moves[-1],
|
||||
setter_card_state=self.get_setter_card_state(step_index),
|
||||
guesser_direction=self._guesser_moves[-1],
|
||||
guesser_card_state=self.get_guesser_card_state(step_index),
|
||||
)
|
||||
|
||||
pub.sendMessage("session.guesser_moved", result=result)
|
||||
|
||||
if self._completed_step_count < self._sequence_length:
|
||||
self._step_state = StepState.WAITING_FOR_SETTER
|
||||
pub.sendMessage(
|
||||
"session.step_state_changed",
|
||||
step_state=self._step_state,
|
||||
)
|
||||
else:
|
||||
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
|
||||
|
||||
def get_setter_card_state(self, step_index: int) -> CardState:
|
||||
setter_step_index = len(self._setter_moves) + 1
|
||||
|
||||
if step_index > setter_step_index:
|
||||
return CardState.SETTER_NOT_SET
|
||||
|
||||
if step_index == setter_step_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, step_index: int) -> CardState:
|
||||
if step_index > self._completed_step_count:
|
||||
return CardState.GUESSER_DISABLED
|
||||
|
||||
elif step_index == self._completed_step_count:
|
||||
return CardState.GUESSER_WAITING
|
||||
|
||||
else:
|
||||
guesser_move = self._guesser_moves[step_index]
|
||||
setter_move = self._setter_moves[step_index]
|
||||
if guesser_move == setter_move:
|
||||
return CardState.GUESSER_CORRECT
|
||||
else:
|
||||
return CardState.GUESSER_WRONG
|
||||
|
||||
@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_lenght(self) -> int:
|
||||
return self._sequence_length
|
||||
|
||||
|
||||
@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, state: SessionState):
|
||||
super().__init__(parent)
|
||||
|
||||
self._state = state
|
||||
|
||||
self._setter_name = self._state.setter_name
|
||||
self._guesser_name = self._state.guesser_name
|
||||
self._sequence_length = self._state.sequence_lenght
|
||||
|
||||
self._setter_sequence: list[Card] = []
|
||||
self._guesser_sequence: list[Card] = []
|
||||
|
||||
self._statusbar = self._get_statusbar()
|
||||
|
||||
self._setup()
|
||||
self._fit_parent_frame()
|
||||
|
||||
pub.subscribe(self._on_setter_moved, "session.setter_moved")
|
||||
pub.subscribe(self._on_guesser_moved, "session.guesser_moved")
|
||||
pub.subscribe(self._on_step_state_changed, "session.step_state_changed")
|
||||
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 _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:
|
||||
pass
|
||||
|
||||
def _on_setter_moved(self, result: MoveResult):
|
||||
self.update_setter_card(
|
||||
index=result.step_index,
|
||||
state=result.setter_card_state or CardState.SETTER_NOT_SET,
|
||||
direction=result.setter_direction,
|
||||
)
|
||||
|
||||
def _on_guesser_moved(self, result: MoveResult):
|
||||
self.update_setter_card(
|
||||
index=result.step_index,
|
||||
state=result.setter_card_state or CardState.SETTER_NOT_SET,
|
||||
direction=result.setter_direction,
|
||||
)
|
||||
|
||||
self.update_guesser_card(
|
||||
index=result.step_index,
|
||||
state=result.guesser_card_state or CardState.GUESSER_DISABLED,
|
||||
direction=result.guesser_direction,
|
||||
)
|
||||
|
||||
def _on_step_state_changed(self, step_state: StepState):
|
||||
pass
|
||||
|
||||
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])
|
||||
|
||||
|
||||
class TestFrame(wx.Frame):
|
||||
def __init__(self):
|
||||
super().__init__(None, title="TestFrame")
|
||||
|
||||
local = Player("Бывалый", PlayerRole.GUESSER)
|
||||
remote = Player("Балбес", PlayerRole.SETTER)
|
||||
session = SessionState(local, remote)
|
||||
|
||||
main_panel = SessionPanel(self, session)
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
main_sizer.Add(main_panel, 1, wx.EXPAND)
|
||||
self.SetSizer(main_sizer)
|
||||
|
||||
s = SessionStats(20, 1, 0, 1, 5.1)
|
||||
main_panel.update_statusbar(None, s)
|
||||
|
||||
session.setter_move(ArrowDirection.LEFT)
|
||||
session.guesser_move(ArrowDirection.LEFT)
|
||||
session.setter_move(ArrowDirection.LEFT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
enable_high_dpi_support()
|
||||
app = wx.App()
|
||||
frame = TestFrame()
|
||||
frame.Show()
|
||||
app.MainLoop()
|
||||
Reference in New Issue
Block a user