"""Game strategies for different contest types.""" import random from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any, Dict, List, Optional from aiogram import types from app.services.contests.enums import GameType @dataclass class GameRenderResult: """Result of rendering a game.""" text: str keyboard: types.InlineKeyboardMarkup requires_text_input: bool = False @dataclass class AnswerCheckResult: """Result of checking user's answer.""" is_correct: bool response_text: str class BaseGameStrategy(ABC): """Base class for game strategies.""" game_type: GameType @abstractmethod def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: """Build round-specific payload from template config.""" pass @abstractmethod def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: """Render game UI for user.""" pass @abstractmethod def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: """Check if user's answer is correct.""" pass def _get_back_button(self, language: str, callback: str) -> types.InlineKeyboardButton: from app.localization.texts import get_texts texts = get_texts(language) return types.InlineKeyboardButton(text=texts.BACK, callback_data=callback) def _get_texts(self, language: str): from app.localization.texts import get_texts return get_texts(language) class QuestButtonsStrategy(BaseGameStrategy): """3x3 grid game - find the secret button.""" game_type = GameType.QUEST_BUTTONS def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: rows = template_payload.get("rows", 3) cols = template_payload.get("cols", 3) total = rows * cols secret_idx = random.randint(0, total - 1) return {"rows": rows, "cols": cols, "secret_idx": secret_idx} def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: texts = self._get_texts(language) rows = payload.get("rows", 3) cols = payload.get("cols", 3) keyboard_rows = [] for r in range(rows): row_buttons = [] for c in range(cols): idx = r * cols + c row_buttons.append( types.InlineKeyboardButton( text="🎛", callback_data=f"contest_pick_{round_id}_quest_{idx}", ) ) keyboard_rows.append(row_buttons) keyboard_rows.append([self._get_back_button(language, back_callback)]) return GameRenderResult( text=texts.t("CONTEST_QUEST_PROMPT", "Выбери один из узлов 3×3:"), keyboard=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows), ) def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: secret_idx = payload.get("secret_idx") try: if user_answer.startswith("quest_"): idx = int(user_answer.split("_")[1]) is_correct = secret_idx is not None and idx == secret_idx else: is_correct = False except (ValueError, IndexError): is_correct = False responses = ["Пусто", "Ложный сервер", "Найди другой узел"] return AnswerCheckResult( is_correct=is_correct, response_text="" if is_correct else random.choice(responses), ) class LockHackStrategy(BaseGameStrategy): """20 locks game - find the hacked one.""" game_type = GameType.LOCK_HACK def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: total = template_payload.get("buttons", 20) secret_idx = random.randint(0, max(0, total - 1)) return {"total": total, "secret_idx": secret_idx} def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: texts = self._get_texts(language) total = payload.get("total", 20) keyboard_rows = [] row = [] for i in range(total): row.append( types.InlineKeyboardButton( text="🔒", callback_data=f"contest_pick_{round_id}_locks_{i}", ) ) if len(row) == 5: keyboard_rows.append(row) row = [] if row: keyboard_rows.append(row) keyboard_rows.append([self._get_back_button(language, back_callback)]) return GameRenderResult( text=texts.t("CONTEST_LOCKS_PROMPT", "Найди взломанную кнопку среди замков:"), keyboard=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows), ) def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: secret_idx = payload.get("secret_idx") try: if user_answer.startswith("locks_"): idx = int(user_answer.split("_")[1]) is_correct = secret_idx is not None and idx == secret_idx else: is_correct = False except (ValueError, IndexError): is_correct = False responses = ["Заблокировано", "Попробуй ещё", "Нет доступа"] return AnswerCheckResult( is_correct=is_correct, response_text="" if is_correct else random.choice(responses), ) class ServerLotteryStrategy(BaseGameStrategy): """Flag lottery game - pick the correct server flag.""" game_type = GameType.SERVER_LOTTERY DEFAULT_FLAGS = ["🇸🇪", "🇸🇬", "🇺🇸", "🇷🇺", "🇩🇪", "🇯🇵", "🇧🇷", "🇦🇺", "🇨🇦", "🇫🇷"] def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: flags = template_payload.get("flags") or self.DEFAULT_FLAGS secret_idx = random.randint(0, len(flags) - 1) return {"flags": flags, "secret_idx": secret_idx} def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: texts = self._get_texts(language) flags = payload.get("flags") or [] shuffled_flags = flags.copy() random.shuffle(shuffled_flags) keyboard_rows = [] row = [] for flag in shuffled_flags: row.append( types.InlineKeyboardButton( text=flag, callback_data=f"contest_pick_{round_id}_{flag}", ) ) if len(row) == 5: keyboard_rows.append(row) row = [] if row: keyboard_rows.append(row) keyboard_rows.append([self._get_back_button(language, back_callback)]) return GameRenderResult( text=texts.t("CONTEST_SERVER_PROMPT", "Выбери сервер:"), keyboard=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows), ) def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: secret_idx = payload.get("secret_idx") flags = payload.get("flags") or [] correct_flag = flags[secret_idx] if secret_idx is not None and secret_idx < len(flags) else "" is_correct = user_answer == correct_flag responses = ["Сервер перегружен", "Нет ответа", "Попробуй завтра"] return AnswerCheckResult( is_correct=is_correct, response_text="" if is_correct else random.choice(responses), ) class BlitzReactionStrategy(BaseGameStrategy): """Blitz reaction game - press button quickly.""" game_type = GameType.BLITZ_REACTION def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: return {"timeout_seconds": template_payload.get("timeout_seconds", 10)} def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: texts = self._get_texts(language) keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text=texts.t("CONTEST_BLITZ_BUTTON", "Я здесь!"), callback_data=f"contest_pick_{round_id}_blitz", ) ], [self._get_back_button(language, back_callback)], ] ) return GameRenderResult( text=texts.t("CONTEST_BLITZ_PROMPT", "⚡️ Блиц! Нажми «Я здесь!»"), keyboard=keyboard, ) def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: is_correct = user_answer == "blitz" return AnswerCheckResult( is_correct=is_correct, response_text="" if is_correct else "Время вышло", ) class LetterCipherStrategy(BaseGameStrategy): """Letter cipher game - decode word from letter codes.""" game_type = GameType.LETTER_CIPHER DEFAULT_WORDS = ["VPN", "SERVER", "PROXY", "XRAY"] def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: words = template_payload.get("words") or self.DEFAULT_WORDS word = random.choice(words) codes = [str(ord(ch.upper()) - 64) for ch in word if ch.isalpha()] return {"question": "-".join(codes), "answer": word.upper()} def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: texts = self._get_texts(language) question = payload.get("question", "") from app.keyboards.inline import get_back_keyboard return GameRenderResult( text=texts.t("CONTEST_CIPHER_PROMPT", "Расшифруй: {q}").format(q=question), keyboard=get_back_keyboard(language), requires_text_input=True, ) def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: correct = (payload.get("answer") or "").upper() is_correct = correct and user_answer.strip().upper() == correct return AnswerCheckResult( is_correct=is_correct, response_text="" if is_correct else "Неверно, попробуй в следующем раунде", ) class EmojiGuessStrategy(BaseGameStrategy): """Emoji guess game - guess service by emoji.""" game_type = GameType.EMOJI_GUESS def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: pairs = template_payload.get("pairs") or [{"question": "🔐📡🌐", "answer": "VPN"}] pair = random.choice(pairs) return pair def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: texts = self._get_texts(language) question = payload.get("question", "🤔") emoji_list = question.split() random.shuffle(emoji_list) shuffled_question = " ".join(emoji_list) from app.keyboards.inline import get_back_keyboard return GameRenderResult( text=texts.t("CONTEST_EMOJI_PROMPT", "Угадай сервис по эмодзи: {q}").format( q=shuffled_question ), keyboard=get_back_keyboard(language), requires_text_input=True, ) def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: correct = (payload.get("answer") or "").upper() is_correct = correct and user_answer.strip().upper() == correct return AnswerCheckResult( is_correct=is_correct, response_text="" if is_correct else "Неверно, попробуй в следующем раунде", ) class AnagramStrategy(BaseGameStrategy): """Anagram game - unscramble letters to form a word.""" game_type = GameType.ANAGRAM DEFAULT_WORDS = ["SERVER", "XRAY", "VPN"] def build_payload(self, template_payload: Dict[str, Any]) -> Dict[str, Any]: words = template_payload.get("words") or self.DEFAULT_WORDS word = random.choice(words).upper() shuffled = "".join(random.sample(word, len(word))) return {"letters": shuffled, "answer": word} def render( self, round_id: int, payload: Dict[str, Any], language: str, back_callback: str = "contests_menu", ) -> GameRenderResult: texts = self._get_texts(language) letters = payload.get("letters", "") from app.keyboards.inline import get_back_keyboard return GameRenderResult( text=texts.t("CONTEST_ANAGRAM_PROMPT", "Составь слово: {letters}").format( letters=letters ), keyboard=get_back_keyboard(language), requires_text_input=True, ) def check_answer( self, user_answer: str, payload: Dict[str, Any], language: str, ) -> AnswerCheckResult: correct = (payload.get("answer") or "").upper() is_correct = correct and user_answer.strip().upper() == correct return AnswerCheckResult( is_correct=is_correct, response_text="" if is_correct else "Неверно, попробуй в следующем раунде", ) # Registry of game strategies _GAME_STRATEGIES: Dict[GameType, BaseGameStrategy] = { GameType.QUEST_BUTTONS: QuestButtonsStrategy(), GameType.LOCK_HACK: LockHackStrategy(), GameType.SERVER_LOTTERY: ServerLotteryStrategy(), GameType.BLITZ_REACTION: BlitzReactionStrategy(), GameType.LETTER_CIPHER: LetterCipherStrategy(), GameType.EMOJI_GUESS: EmojiGuessStrategy(), GameType.ANAGRAM: AnagramStrategy(), } def get_game_strategy(game_type: GameType | str) -> Optional[BaseGameStrategy]: """Get game strategy by type.""" if isinstance(game_type, str): try: game_type = GameType(game_type) except ValueError: return None return _GAME_STRATEGIES.get(game_type) def get_all_game_types() -> List[GameType]: """Get list of all supported game types.""" return list(_GAME_STRATEGIES.keys())