from __future__ import annotations from dataclasses import dataclass from typing import Optional, Union @dataclass class ClientHello: description: str class ClientQueue: pass @dataclass class ClientMove: square: Optional[int] # None for initial move MOVE~M m_value: int # 0-17 @dataclass class UnknownCommand: raw: str ClientCommand = Union[ClientHello, ClientQueue, ClientMove, UnknownCommand] def parse_client_line(line: str) -> ClientCommand: parts = line.strip().split("~") if not parts: return UnknownCommand(raw=line) cmd = parts[0] if cmd == "HELLO" and len(parts) == 2: return ClientHello(description=parts[1]) if cmd == "QUEUE" and len(parts) == 1: return ClientQueue() if cmd == "MOVE": if len(parts) == 2: try: m_value = int(parts[1]) except ValueError: return UnknownCommand(raw=line) return ClientMove(square=None, m_value=m_value) if len(parts) == 3: try: square = int(parts[1]) m_value = int(parts[2]) except ValueError: return UnknownCommand(raw=line) return ClientMove(square=square, m_value=m_value) return UnknownCommand(raw=line) def format_hello(description: str) -> str: return f"HELLO~{description}" def format_newgame(player1: str, player2: str) -> str: return f"NEWGAME~{player1}~{player2}" def format_move_broadcast(square: Optional[int], m_value: int) -> str: if square is None: return f"MOVE~{m_value}" return f"MOVE~{square}~{m_value}" def format_gameover(reason: str, winner: Optional[str]) -> str: if winner is None: return f"GAMEOVER~{reason}" return f"GAMEOVER~{reason}~{winner}"