/
grigorovich
/
COSIN-K
Обзор
Документация
Войти
/
grigorovich
/
COSIN-K
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
controller/game_controller.py
493 строки
18 KB
grigorovich
deepseek
13 фев 2026, 06:07
13 фев 2026, 06:07
e130d21
Код
Авторство
О чём код?
""" GameController — посредник между Model и View. Обрабатывает команды пользователя и обновляет состояние. """ from typing import Optional, Dict, Any from model import SolitaireEngine, GameState, GameFactory from view import GameView class GameController: """ Контроллер связывает Model (SolitaireEngine) и View (GameView). Получает события от View, управляет Model, уведомляет View об изменениях. """ def __init__(self, engine: SolitaireEngine, view: GameView): self.engine = engine self.view = view # Связываем View с контроллером self.view.controller = self # Состояние выбора для многошаговых операций (ТОЛЬКО В КОНТРОЛЛЕРЕ!) self._selected_pile: Optional[str] = None self._selected_count: int = 1 # Подписываемся на события Engine self._setup_engine_listeners() def _setup_engine_listeners(self) -> None: """Подписаться на события от Engine.""" self.engine.add_listener(self._on_engine_event) def _on_engine_event(self, event: str, data: Dict[str, Any]) -> None: """Обработчик событий от Engine.""" handlers = { "game_started": self._on_game_started, "move_made": self._on_move_made, "draw": self._on_draw, "recycle": self._on_recycle, "game_won": self._on_game_won, "undo": self._on_undo, "redo": self._on_redo, } handler = handlers.get(event) if handler: handler(data) def _on_game_started(self, data: Dict[str, Any]) -> None: """Обработка начала игры.""" self.view.show_message("New game started!", "info") self.update_view() def _on_move_made(self, data: Dict[str, Any]) -> None: """Обработка хода.""" self._clear_selection() self.update_view() def _on_draw(self, data: Dict[str, Any]) -> None: """Обработка взятия карт.""" count = data.get("count", 1) self.view.show_message(f"Drew {count} card(s)", "info") self.update_view() def _on_recycle(self, data: Dict[str, Any]) -> None: """Обработка перебора колоды.""" count = data.get("count", 0) self.view.show_message(f"Recycled {count} card(s)", "info") self.update_view() def _on_game_won(self, data: Dict[str, Any]) -> None: """Обработка победы.""" score = data.get("score", 0) self.view.show_message(f"🎉 You won! Final score: {score}", "win") self.update_view() def _on_undo(self, data: Dict[str, Any]) -> None: """Обработка отмены хода.""" self.view.show_message("Undo successful", "success") self.update_view() def _on_redo(self, data: Dict[str, Any]) -> None: """Обработка повтора хода.""" self.view.show_message("Redo successful", "success") self.update_view() # === Публичные методы для View === def _parse_pile_name(self, name: str) -> str: """0 → tableau_0, h → foundation_HEARTS, w → waste, t3 → tableau_3""" name = name.lower().strip() # Цифры → tableau if name.isdigit(): return f"tableau_{name}" # t0, t1 → tableau if name.startswith('t') and name[1:].isdigit(): return f"tableau_{name[1:]}" # Масти → foundation suit_map = { 'h': 'HEARTS', 'd': 'DIAMONDS', 'c': 'CLUBS', 's': 'SPADES' } if name in suit_map: return f"foundation_{suit_map[name]}" # Специальные стопки if name in ('w', 'waste'): return 'waste' if name in ('st', 'stock'): return 'stock' # Полное имя — НЕ ИЗМЕНЯЕМ! return name # ← уже полное имя, не трогаем def update_view(self) -> None: """Попросить View отобразить текущее состояние.""" if self.engine.state: # Передаём в View дополнительно информацию о выбранной стопке self.view.display_state( self.engine.state, selected_pile=self._selected_pile, selected_count=self._selected_count ) def handle_command(self, command: str) -> None: """Обработать команду от пользователя.""" if not command: return parts = command.split() cmd = parts[0].lower() args = parts[1:] # 🔥 Проверяем шорткаты через правила игры if self.engine and self.engine.rules: if hasattr(self.engine.rules, 'validate_shortcut'): shortcut = self.engine.rules.validate_shortcut(command) if shortcut: from_pile, to_pile, count = shortcut return self._cmd_move([from_pile, to_pile, str(count)]) # 🔥 Команда из одной цифры - авто-ход из tableau if len(cmd) == 1 and cmd.isdigit(): if not args: return self._cmd_quick_move(cmd) # 🔥 Команда 'w' - авто-ход из waste if len(cmd) == 1 and cmd == 'w': if not args: return self._cmd_quick_waste() # 🔥 Команда '?' - справка if cmd == '?': return self._cmd_help([]) # Нормализация алиасов cmd_map = { 's': 'select', 'select': 'select', 'm': 'move', 'move': 'move', 'd': 'draw', 'draw': 'draw', 'u': 'undo', 'undo': 'undo', 'r': 'redo', 'redo': 'redo', 'n': 'new', 'new': 'new', 'q': 'quit', 'quit': 'quit', 'h': 'help', 'help': 'help', '?': 'help', # добавляем '?' } normalized_cmd = cmd_map.get(cmd, cmd) handlers = { 'select': self._cmd_select, 'move': self._cmd_move, 'draw': self._cmd_draw, 'undo': self._cmd_undo, 'redo': self._cmd_redo, 'new': self._cmd_new, 'quit': self._cmd_quit, 'help': self._cmd_help, } handler = handlers.get(normalized_cmd, self._cmd_unknown) handler(args) # === Обработчики команд === def _cmd_select(self, args: list) -> None: """Выбор стопки для многошагового хода.""" if not args: self.view.show_message("Usage: select <pile_name> [count]", "error") return # 🔥 ПРЕОБРАЗУЕМ КОРОТКОЕ ИМЯ pile_name = self._parse_pile_name(args[0]) try: count = int(args[1]) if len(args) > 1 else 1 except ValueError: self.view.show_message("Count must be a number", "error") return if not self.engine.state: return pile = self.engine.state.get_pile(pile_name) if pile is None: self.view.show_message(f"Unknown pile: {pile_name}", "error") return # Если нет выбранной — выбираем источник if self._selected_pile is None: if pile.is_empty(): self.view.show_message("Cannot select empty pile", "error") return face_up = pile.face_up_count() if count > face_up: self.view.show_message(f"Only {face_up} cards available", "error") return self._selected_pile = pile_name self._selected_count = count self.view.show_message(f"Selected {pile_name} ({count} card(s))", "info") self.update_view() # Иначе — выбираем назначение и выполняем ход else: success = self.engine.move(self._selected_pile, pile_name, self._selected_count) if not success: self.view.show_message( f"Cannot move {self._selected_count} card(s) " f"from {self._selected_pile} to {pile_name}", "error" ) self._clear_selection() # View обновится по событию от Engine def _cmd_move(self, args: list) -> None: """Прямой ход: from to [count].""" if len(args) < 2: self.view.show_message("Usage: move <from_pile> <to_pile> [count]", "error") return from_pile = self._parse_pile_name(args[0]) to_pile = self._parse_pile_name(args[1]) try: count = int(args[2]) if len(args) > 2 else 1 except ValueError: self.view.show_message("Count must be a number", "error") return success = self.engine.move(from_pile, to_pile, count) if not success: self.view.show_message("Invalid move!", "error") self._clear_selection() # View обновится по событию от Engine def _cmd_draw(self, args: list) -> None: """Взять карту из колоды.""" if not self.engine.state: return success = self.engine.draw() if not success: self.view.show_message("Cannot draw more cards", "error") self._clear_selection() # View обновится по событию от Engine def _cmd_undo(self, args: list) -> None: """Отменить последний ход.""" success = self.engine.undo() if not success: self.view.show_message("Nothing to undo", "error") self._clear_selection() # View обновится по событию от Engine def _cmd_redo(self, args: list) -> None: """Повторить отменённый ход.""" success = self.engine.redo() if not success: self.view.show_message("Nothing to redo", "error") self._clear_selection() # View обновится по событию от Engine def _cmd_new(self, args: list) -> None: """Новая игра.""" variant = args[0] if args else "klondike" # Проверяем существование варианта if not GameFactory.is_available(variant): self.view.show_message(f"Unknown variant: {variant}", "error") self.view.show_message(f"Available: {', '.join(GameFactory.available_games())}", "info") return if self.engine.state and self.engine.state.moves_count > 0: if not self.view.ask_confirm("Abandon current game?"): return self._clear_selection() self.engine.new_game(variant) # ← передаём вариант! # View обновится по событию от Engine def _cmd_quit(self, args: list) -> None: """Выход из игры.""" if self.engine.state and self.engine.state.moves_count > 0: if self.view.ask_confirm("Save game before quitting?"): # TODO: реализовать сохранение pass self.view.stop() def _cmd_quick_move(self, pile_num: str) -> None: """Авто-ход из tableau_n в лучшее доступное место.""" if not self.engine or not self.engine.state: return from_pile = f"tableau_{pile_num}" source = self.engine.state.get_pile(from_pile) if not source or source.is_empty(): self.view.show_message(f"Pile {from_pile} is empty", "error") return if not hasattr(self.engine.rules, 'get_available_moves'): self.view.show_message("Hint system not available", "error") return # Получаем все возможные ходы available_moves = self.engine.rules.get_available_moves(self.engine.state) # Фильтруем ходы из указанной стопки moves_from_pile = [ move for move in available_moves if move.from_pile == from_pile ] if not moves_from_pile: self.view.show_message(f"No available moves from {from_pile}", "error") return # Приоритет: foundation > tableau (более правые колонки) foundation_moves = [] tableau_moves = [] for move in moves_from_pile: if move.to_pile.startswith("foundation_"): foundation_moves.append(move) elif move.to_pile.startswith("tableau_"): tableau_moves.append(move) selected_move = None if foundation_moves: selected_move = foundation_moves[0] self.view.show_message(f"Auto-moving to foundation", "info") elif tableau_moves: # Сортируем tableau по номеру колонки (по убыванию - вправо) tableau_moves.sort(key=lambda m: int(m.to_pile.split('_')[1]), reverse=True) selected_move = tableau_moves[0] self.view.show_message(f"Auto-moving to {selected_move.to_pile}", "info") if selected_move: success = self.engine.move( selected_move.from_pile, selected_move.to_pile, len(selected_move.cards) ) if not success: self.view.show_message("Auto-move failed!", "error") else: self.view.show_message(f"No suitable move from {from_pile}", "error") def _cmd_quick_waste(self) -> None: """Авто-ход из waste в лучшее доступное место.""" if not self.engine or not self.engine.state: return from_pile = "waste" source = self.engine.state.get_pile(from_pile) if not source or source.is_empty(): self.view.show_message("Waste is empty", "error") return if not hasattr(self.engine.rules, 'get_available_moves'): self.view.show_message("Hint system not available", "error") return # Получаем все возможные ходы available_moves = self.engine.rules.get_available_moves(self.engine.state) # Фильтруем ходы из waste moves_from_waste = [ move for move in available_moves if move.from_pile == from_pile ] if not moves_from_waste: self.view.show_message("No available moves from waste", "error") return # Приоритет: foundation > tableau (более правые колонки) foundation_moves = [] tableau_moves = [] for move in moves_from_waste: if move.to_pile.startswith("foundation_"): foundation_moves.append(move) elif move.to_pile.startswith("tableau_"): tableau_moves.append(move) selected_move = None if foundation_moves: selected_move = foundation_moves[0] self.view.show_message("Auto-moving waste to foundation", "info") elif tableau_moves: # Сортируем tableau по номеру колонки (по убыванию - вправо) tableau_moves.sort(key=lambda m: int(m.to_pile.split('_')[1]), reverse=True) selected_move = tableau_moves[0] self.view.show_message(f"Auto-moving waste to {selected_move.to_pile}", "info") if selected_move: success = self.engine.move( selected_move.from_pile, selected_move.to_pile, len(selected_move.cards) # всегда 1 для waste ) if not success: self.view.show_message("Auto-move from waste failed!", "error") else: self.view.show_message("No suitable move from waste", "error") def _cmd_help(self, args: list) -> None: """Показать полную справку.""" if not self.engine or not self.engine.rules: # Базовая справка, если игры нет variants = ", ".join(GameFactory.available_games()) help_text = f""" === Solitaire Game Controller === Available games: {variants} Start a game with: new [variant] Type 'help' or '?' for game-specific commands. """ self.view.show_message(help_text, "info") return # Получаем справку от правил текущей игры rules = self.engine.rules help_text = rules.get_help_text() game_help = rules.get_game_help() if game_help: help_text += "\n" + game_help shortcuts = rules.get_shortcuts_text() if shortcuts: help_text += "\n" + shortcuts quick_moves = rules.get_quick_moves_text() if quick_moves: help_text += "\n" + quick_moves variants = ", ".join(GameFactory.available_games()) help_text += f"\n\nAvailable variants: {variants}" help_text += "\n\nTip: Type '?' for this help anytime." self.view.show_message(help_text, "info") def _cmd_unknown(self, args: list) -> None: """Неизвестная команда.""" self.view.show_message("Unknown command. Type 'help' for available commands.", "error") # === Вспомогательные методы === def _clear_selection(self) -> None: """Сбросить выбор стопки.""" self._selected_pile = None self._selected_count = 1 # self.update_view() # показываем снятие выделения