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])