/
anstyfil
/
Minesweeper
Обзор
Документация
Войти
/
anstyfil
/
Minesweeper
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
UI/Menu.cs
563 строки
23 KB
Elizaveta
add history command to console interface and help text
18 дек 2025, 21:35
18 дек 2025, 21:35
fb797c7
Код
Авторство
О чём код?
using System; using System.Text; using System.Threading; using Minesweeper.Game; using Minesweeper.Storage; namespace Minesweeper.UI { /// <summary> /// Статический класс для главного меню игры. /// Предоставляет опции: новая игра, загрузка игры, выход. /// </summary> public static class Menu { /// <summary> /// Запускает главное меню и управляет игровым циклом. /// </summary> public static void Run() { while (true) { Console.WriteLine("=== МЕНЮ ==="); Console.WriteLine("1. Новая игра"); Console.WriteLine("2. Загрузить игру"); Console.WriteLine("3. Показать рекорды"); Console.WriteLine("4. Выход"); Console.Write("Выберите опцию: "); string? input = Console.ReadLine(); if (string.IsNullOrWhiteSpace(input)) { continue; } switch (input.Trim()) { case "1": StartNewGame(); break; case "2": LoadGame(); break; case "3": ShowRecords(); break; case "4": Console.WriteLine("До свидания!"); return; default: Console.WriteLine("Неверная опция. Попробуйте снова."); break; } } } /// <summary> /// Создаёт новую игру с параметрами по умолчанию и запускает игровой цикл. /// </summary> private static void StartNewGame() { // Запрашиваем имена игроков ПЕРЕД созданием игры Console.WriteLine("\n=== ВВОД ИМЕН ИГРОКОВ ==="); string player1Name = GetValidPlayerName("Введите имя игрока 1: "); string player2Name = GetValidPlayerName("Введите имя игрока 2: "); // Параметры по умолчанию const int width = 10; const int height = 10; const int mineCount = 20; var config = new GameConfig(width, height, mineCount); var geometry = new BoardGeometry(config); var board = new BoardState(geometry); board.InitializeMines(mineCount); board.ClearHistory(); // Устанавливаем имена игроков board.Player1Name = player1Name; board.Player2Name = player2Name; // Инициализируем таймер board.InitializeTimer(); Console.WriteLine($"\nСоздана новая игра: {width}x{height}, мин: {mineCount}"); if (board.IsTimerEnabled) { Console.WriteLine($"⏰ На каждый ход дается {board.TimePerMoveSeconds} секунд. Если время истечет, ход перейдет к другому игроку.\n"); } else { Console.WriteLine(); } RunGameLoop(board); } /// <summary> /// Запрашивает у пользователя имя игрока с валидацией. /// Повторяет запрос, пока не будет введено валидное имя. /// </summary> /// <param name="prompt">Текст приглашения для ввода</param> /// <returns>Валидное имя игрока</returns> private static string GetValidPlayerName(string prompt) { while (true) { Console.Write(prompt); string? name = Console.ReadLine(); // Проверка на пустую строку if (string.IsNullOrWhiteSpace(name)) { Console.WriteLine("Ошибка: Имя не может быть пустым. Попробуйте снова."); continue; } // Валидация через PlayerNameValidator var (isValid, errorMessage) = PlayerNameValidator.Validate(name); if (isValid) { return name; } // Если имя невалидно, показываем ошибку и повторяем запрос Console.WriteLine($"Ошибка: {errorMessage}"); } } /// <summary> /// Загружает игру из файла и запускает игровой цикл. /// </summary> private static void LoadGame() { Console.Write("Введите путь к файлу сохранения: "); string? filePath = Console.ReadLine(); if (string.IsNullOrWhiteSpace(filePath)) { Console.WriteLine("Путь к файлу не может быть пустым."); return; } BoardState? board = GameLoader.Load(filePath); if (board == null) { Console.WriteLine("Не удалось загрузить игру. Проверьте путь к файлу и формат данных."); return; } Console.WriteLine("\nИгра успешно загружена."); if (board.IsTimerEnabled) { Console.WriteLine($"⏰ На каждый ход дается {board.TimePerMoveSeconds} секунд. Если время истечет, ход перейдет к другому игроку.\n"); } else { Console.WriteLine(); } // Инициализируем таймер для загруженной игры board.InitializeTimer(); RunGameLoop(board); } /// <summary> /// Запускает игровой цикл: отрисовка поля, ввод команд, выполнение ходов. /// </summary> private static void RunGameLoop(BoardState board) { // Запускаем таймер сразу при старте игры if (board.TurnTimer != null) { board.TurnTimer.Start(); } // Создаем обработчик события (сохраняем ссылку для правильной отписки) Action? timeExpiredHandler = null; if (board.TurnTimer != null) { timeExpiredHandler = () => OnTimeExpired(board); board.TurnTimer.TimeExpired += timeExpiredHandler; } while (!board.GameOver) { // Очистка экрана для лучшей читаемости Console.Clear(); // Отрисовка поля и статуса Console.WriteLine("=== САПЁР ===\n"); GameRenderer.RenderBoard(board); Console.WriteLine(); GameRenderer.RenderStatus(board); Console.WriteLine(); Console.WriteLine($"Команда: reveal x y (координаты от 1 до {board.Geometry.Width}, например: reveal 3 5)"); Console.WriteLine("Для просмотра истории ходов: history (или h)"); Console.WriteLine("Для сохранения игры: save <filename>"); Console.WriteLine("Для выхода в меню введите: exit"); if (board.IsTimerEnabled && board.TurnTimer != null) { Console.WriteLine($"⏰ Время на ход: {board.TimePerMoveSeconds} секунд"); } Console.WriteLine(); // Ввод команды // Таймер работает в фоне и автоматически переключаеtт ход при истечении времени Console.Write("> "); string? input = Console.ReadLine(); if (string.IsNullOrWhiteSpace(input)) { continue; } // Проверка на выход if (input.Trim().ToLower() == "exit") { // Отписываемся от события таймера if (board.TurnTimer != null && timeExpiredHandler != null) { board.TurnTimer.TimeExpired -= timeExpiredHandler; } // Останавливаем таймер при выходе из игры board.DisposeTimer(); return; } // Проверка на команду history string lowerInput = input.Trim().ToLower(); if (lowerInput == "history" || lowerInput == "h") { ShowHistory(board); continue; // Продолжаем цикл игры без выполнения хода } // Парсинг команды CommandResult? commandResult = InputValidator.ParseCommand(input); if (commandResult == null) { // Приостанавливаем таймер при показе ошибки if (board.TurnTimer != null) { board.TurnTimer.Pause(); } string? error = InputValidator.GetLastParseError(); Console.WriteLine($"Ошибка: {error}"); Console.WriteLine("Нажмите Enter для продолжения..."); Console.ReadLine(); // Возобновляем таймер после нажатия Enter if (board.TurnTimer != null) { board.TurnTimer.Resume(); } continue; } // Обработка команды сохранения if (commandResult.Value.Command == "save") { string? filePath = commandResult.Value.FilePath; if (string.IsNullOrWhiteSpace(filePath)) { // Приостанавливаем таймер при показе ошибки if (board.TurnTimer != null) { board.TurnTimer.Pause(); } Console.WriteLine("Ошибка: укажите путь к файлу для сохранения"); Console.WriteLine("Нажмите Enter для продолжения..."); Console.ReadLine(); // Возобновляем таймер после нажатия Enter if (board.TurnTimer != null) { board.TurnTimer.Resume(); } continue; } // Приостанавливаем таймер перед сохранением if (board.TurnTimer != null) { board.TurnTimer.Pause(); } try { GameSaver.Save(board, filePath); Console.WriteLine($"Игра успешно сохранена в файл: {filePath}"); Console.WriteLine("Нажмите Enter для продолжения..."); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine($"Ошибка при сохранении: {ex.Message}"); Console.WriteLine("Нажмите Enter для продолжения..."); Console.ReadLine(); } finally { // Возобновляем таймер после сохранения (даже если была ошибка) if (board.TurnTimer != null) { board.TurnTimer.Resume(); } } continue; } // Валидация координат (только для команды reveal) string? validationError = InputValidator.Validate(board, commandResult.Value.X, commandResult.Value.Y); if (validationError != null) { // Приостанавливаем таймер при показе ошибки if (board.TurnTimer != null) { board.TurnTimer.Pause(); } Console.WriteLine($"Ошибка: {validationError}"); Console.WriteLine("Нажмите Enter для продолжения..."); Console.ReadLine(); // Возобновляем таймер после нажатия Enter if (board.TurnTimer != null) { board.TurnTimer.Resume(); } continue; } // Выполнение хода GameLogic.Reveal(board, commandResult.Value.X, commandResult.Value.Y); } // Игра завершена - останавливаем таймер и отписываемся от событий СРАЗУ // Это нужно сделать до показа финального состояния, чтобы избежать срабатывания TimeExpired if (board.TurnTimer != null) { // Отписываемся от события таймера if (timeExpiredHandler != null) { board.TurnTimer.TimeExpired -= timeExpiredHandler; } // Останавливаем таймер board.TurnTimer.Stop(); } // Игра завершена - показываем финальное состояние Console.Clear(); Console.WriteLine("=== ИГРА ЗАВЕРШЕНА ===\n"); GameRenderer.RenderBoard(board); Console.WriteLine(); GameRenderer.RenderStatus(board); Console.WriteLine(); if (board.Winner > 0) { Console.WriteLine($"Победитель: Игрок {board.Winner}"); // Сохранение рекорда try { // Определяем имя победителя string winnerName = board.Winner == 1 ? board.Player1Name : board.Player2Name; // Определяем счет победителя int winnerScore = board.Winner == 1 ? board.Score1 : board.Score2; // Создаем новый рекорд var record = new HighScoreRecord { PlayerName = winnerName, Score = winnerScore, Date = DateTime.Now }; // Сохраняем рекорд HighScoreManager.SaveRecord(record); } catch { // Тихая обработка ошибок - не прерываем игру, если не удалось сохранить } } else { Console.WriteLine("Ничья!"); } Console.WriteLine("\nНажмите Enter для возврата в меню..."); Console.ReadLine(); // Таймер уже остановлен и отписан выше, но на всякий случай очищаем board.DisposeTimer(); } /// <summary> /// Обрабатывает событие истечения времени на ход. /// </summary> /// <param name="board">Состояние игрового поля</param> private static void OnTimeExpired(BoardState board) { // Проверяем, не завершена ли игра - если да, игнорируем событие if (board.GameOver) { return; } // Определяем имя игрока, который пропустил ход (ДО переключения) int currentPlayer = board.CurrentPlayer; string playerName = currentPlayer == 1 ? board.Player1Name : board.Player2Name; if (string.IsNullOrEmpty(playerName)) { playerName = $"Игрок {currentPlayer}"; } // Обрабатываем истечение времени (переключает игрока) GameLogic.HandleTimeExpiration(board); // Проверяем еще раз после обработки (игра могла завершиться) if (board.GameOver) { return; } // Очищаем консоль и выводим сообщение Console.Clear(); Console.WriteLine("Время истекло! Ход пропущен."); Console.WriteLine($"Игрок {playerName} пропустил ход из-за истечения времени."); Console.WriteLine("Нажмите Enter для продолжения..."); Console.ReadLine(); } /// <summary> /// Показывает топ-5 рекордов из таблицы рекордов. /// </summary> private static void ShowRecords() { Console.Clear(); Console.WriteLine("=== ТОП-5 РЕКОРДОВ ===\n"); // Загружаем топ-5 рекордов через HighScoreManager var topRecords = HighScoreManager.GetTopN(5); // Проверяем, есть ли рекорды if (topRecords.Count == 0) { Console.WriteLine("Рекордов пока нет. Сыграйте игру, чтобы установить рекорд!"); Console.WriteLine("\nНажмите Enter для возврата в меню..."); Console.ReadLine(); return; } // Отображаем рекорды for (int i = 0; i < topRecords.Count; i++) { var record = topRecords[i]; // Форматируем дату: dd.MM.yyyy string dateString = record.Date.ToString("dd.MM.yyyy"); // Определяем правильную форму слова "очко" string scoreWord; if (record.Score == 1) { scoreWord = "очко"; } else if (record.Score >= 2 && record.Score <= 4) { scoreWord = "очка"; } else { scoreWord = "очков"; } Console.WriteLine($"{i + 1}. {record.PlayerName} - {record.Score} {scoreWord} - {dateString}"); } Console.WriteLine("\nНажмите Enter для возврата в меню..."); Console.ReadLine(); } /// <summary> /// Отображает историю ходов игры в виде таблицы. /// </summary> /// <param name="board">Состояние игрового поля</param> private static void ShowHistory(BoardState board) { if (board == null) { throw new ArgumentNullException(nameof(board)); } // Получаем историю ходов var history = board.MoveHistory; // Если история пустая if (history.Count == 0) { Console.WriteLine("История ходов пуста."); Console.WriteLine(); return; } // Выводим заголовок Console.WriteLine("=== ИСТОРИЯ ХОДОВ ==="); Console.WriteLine(); // Выводим заголовки таблицы Console.WriteLine("Ход | Игрок | Координаты | Открыто | Очки | Время"); Console.WriteLine("----|------------|------------|---------|------|------"); // Выводим каждый ход foreach (var move in history) { // Определяем координаты string coordinates; if (move.X == -1 && move.Y == -1) { coordinates = "Пропуск"; } else { coordinates = $"({move.X + 1}, {move.Y + 1})"; // +1 для отображения координат от 1 } // Определяем время string time; if (move.TimeElapsed == null) { time = "-"; } else { time = $"{move.TimeElapsed}s"; } // Форматируем и выводим строку таблицы Console.WriteLine($"{move.MoveNumber,4} | {move.PlayerName,-10} | {coordinates,-10} | {move.CellsRevealed,7} | {move.PlayerScore,5} | {time,-5}"); } Console.WriteLine(); Console.WriteLine("Нажмите Enter для продолжения..."); Console.ReadLine(); } } }