Текстовая игра на C#
Текстовая игра на C#
Текстовая игра — это классический формат интерактивного программного обеспечения, в котором взаимодействие с пользователем происходит исключительно через текстовый ввод и вывод. Такие игры не требуют графического интерфейса, а игровой процесс строится на описаниях, выборе действий и логике принятия решений. В среде обучения программированию текстовые игры часто используются как практический способ освоить базовые конструкции языка: циклы, условия, методы, коллекции и работу с пользовательским вводом.
В данном примере реализуется консольная текстовая игра на C# с использованием современных возможностей .NET: асинхронности, объектно-ориентированного подхода, инкапсуляции логики и модульной структуры кода. Игра демонстрирует принципы проектирования небольших приложений, которые можно легко расширять и модифицировать.
Архитектура игры
Текстовая игра состоит из следующих ключевых компонентов:
- GameLoop — основной цикл игры, управляющий последовательностью событий;
- GameState — объект, хранящий текущее состояние игры (здоровье героя, уровень, инвентарь и т.д.);
- Narrator — компонент, отвечающий за вывод текстовых сообщений;
- PlayerInput — обработчик пользовательского ввода;
- Scene — абстракция игровой сцены или локации;
- Choice — вариант действия, доступный игроку в текущей сцене.
Такая структура позволяет отделить логику игры от её представления и упрощает добавление новых сцен, персонажей или механик.
Базовые классы и модели
Класс GameState
public class GameState
{
public string PlayerName { get; set; } = "Игрок";
public int Health { get; set; } = 100;
public int Score { get; set; } = 0;
public List<string> Inventory { get; set; } = new();
public string CurrentSceneId { get; set; } = "start";
}
Этот класс содержит всё, что определяет текущее положение игрока в мире игры. Он передаётся между сценами и изменяется по мере развития сюжета.
Класс Choice
public class Choice
{
public string Text { get; set; } = string.Empty;
public string TargetSceneId { get; set; } = string.Empty;
public Action<GameState>? OnSelect { get; set; }
}
Каждый выбор содержит:
- текст, который видит игрок;
- идентификатор следующей сцены;
- необязательное действие, которое выполняется при выборе (например, изменение здоровья или добавление предмета в инвентарь).
Абстрактный класс Scene
public abstract class Scene
{
public string Id { get; init; } = string.Empty;
public abstract string Description { get; }
public abstract List<Choice> GetChoices(GameState state);
}
Каждая конкретная сцена наследуется от этого класса и реализует два метода:
Description— возвращает описание локации;GetChoices— возвращает доступные действия в зависимости от состояния игры.
Реализация конкретных сцен
Стартовая сцена
public class StartScene : Scene
{
public override string Id => "start";
public override string Description =>
"Вы просыпаетесь в тёмной комнате. Перед вами две двери: одна красная, другая синяя.";
public override List<Choice> GetChoices(GameState state)
{
return new()
{
new Choice
{
Text = "Открыть красную дверь",
TargetSceneId = "red_room"
},
new Choice
{
Text = "Открыть синюю дверь",
TargetSceneId = "blue_room"
},
new Choice
{
Text = "Посмотреть инвентарь",
TargetSceneId = "inventory"
}
};
}
}
Сцена красной комнаты
public class RedRoomScene : Scene
{
public override string Id => "red_room";
public override string Description =>
"Вы входите в красную комнату. Здесь жарко, и вы замечаете сундук в углу.";
public override List<Choice> GetChoices(GameState state)
{
var choices = new List<Choice>
{
new Choice
{
Text = "Открыть сундук",
TargetSceneId = "chest",
OnSelect = gameState =>
{
if (!gameState.Inventory.Contains("золотой ключ"))
{
gameState.Inventory.Add("золотой ключ");
gameState.Score += 10;
}
}
},
new Choice
{
Text = "Вернуться в начальную комнату",
TargetSceneId = "start"
}
};
return choices;
}
}
Обратите внимание: при открытии сундука выполняется действие OnSelect, которое проверяет наличие предмета и добавляет его, если его ещё нет. Это предотвращает дублирование и обеспечивает согласованность состояния.
Сцена инвентаря
public class InventoryScene : Scene
{
public override string Id => "inventory";
public override string Description => "Ваш инвентарь пуст.";
public override List<Choice> GetChoices(GameState state)
{
var description = state.Inventory.Count == 0
? "Ваш инвентарь пуст."
: $"В инвентаре: {string.Join(", ", state.Inventory)}.";
// Динамическое обновление описания
_description = description;
return new()
{
new Choice
{
Text = "Назад",
TargetSceneId = state.CurrentSceneId == "inventory" ? "start" : state.CurrentSceneId
}
};
}
private string _description = string.Empty;
public override string Description => _description;
}
Здесь используется простой приём: описание генерируется динамически на основе текущего состояния. Это позволяет отображать актуальную информацию без необходимости хранить её отдельно.
Компонент Narrator
public static class Narrator
{
public static void Say(string message)
{
Console.WriteLine($"\n{message}\n");
}
public static void ShowChoices(List<Choice> choices)
{
for (int i = 0; i < choices.Count; i++)
{
Console.WriteLine($"{i + 1}. {choices[i].Text}");
}
}
}
Narrator отвечает только за вывод. Он не знает ничего о логике игры — это соответствует принципу разделения ответственности.
Обработка ввода
public static class PlayerInput
{
public static async Task<int> ReadChoiceAsync(int maxOptions)
{
while (true)
{
Console.Write("\nВаш выбор (введите номер): ");
var input = await Console.In.ReadLineAsync();
if (int.TryParse(input, out int choice) && choice >= 1 && choice <= maxOptions)
return choice - 1; // индексация с нуля
Console.WriteLine("Неверный ввод. Попробуйте снова.");
}
}
}
Метод ReadChoiceAsync запрашивает у пользователя число, проверяет его корректность и возвращает индекс выбранного варианта. Использование async/await делает код совместимым с потенциально асинхронными источниками ввода (например, сетевыми клиентами в будущем).
Основной игровой цикл
public class GameLoop
{
private readonly Dictionary<string, Scene> _scenes = new();
private readonly GameState _state = new();
public GameLoop()
{
// Регистрация всех сцен
RegisterScene(new StartScene());
RegisterScene(new RedRoomScene());
RegisterScene(new BlueRoomScene());
RegisterScene(new InventoryScene());
RegisterScene(new ChestScene());
// ... другие сцены
}
private void RegisterScene(Scene scene)
{
_scenes[scene.Id] = scene;
}
public async Task RunAsync()
{
Narrator.Say("Добро пожаловать в текстовую игру!");
Console.Write("Введите ваше имя: ");
_state.PlayerName = (await Console.In.ReadLineAsync())?.Trim() ?? "Игрок";
while (_state.Health > 0)
{
if (!_scenes.TryGetValue(_state.CurrentSceneId, out var currentScene))
{
Narrator.Say("Ошибка: неизвестная сцена. Игра завершена.");
break;
}
Narrator.Say(currentScene.Description);
var choices = currentScene.GetChoices(_state);
Narrator.ShowChoices(choices);
var selectedIndex = await PlayerInput.ReadChoiceAsync(choices.Count);
var selectedChoice = choices[selectedIndex];
// Выполнение действия при выборе
selectedChoice.OnSelect?.Invoke(_state);
// Переход к новой сцене
_state.CurrentSceneId = selectedChoice.TargetSceneId;
// Простая проверка завершения
if (_state.CurrentSceneId == "end")
{
Narrator.Say($"Игра окончена! Ваш счёт: {_state.Score}");
break;
}
}
if (_state.Health <= 0)
{
Narrator.Say("Вы погибли. Игра окончена.");
}
}
}
Цикл игры:
- Запрашивает имя игрока.
- Пока здоровье больше нуля:
- загружает текущую сцену;
- выводит описание;
- показывает варианты выбора;
- читает ввод;
- выполняет действие (если есть);
- обновляет текущую сцену.
- Завершает игру при достижении финальной сцены или смерти.
Запуск игры
// Program.cs
var game = new GameLoop();
await game.RunAsync();
В проекте .NET 6+ или выше достаточно поместить этот код в файл Program.cs. Приложение будет работать как полноценная консольная программа.
Расширяемость и модификация
Такая архитектура легко расширяется:
- Добавление новых сцен: создаётся новый класс, наследующий
Scene, и регистрируется вGameLoop. - Сложные условия: в
GetChoicesможно добавлять проверки (if (state.Inventory.Contains("ключ"))). - Случайные события: использование
Randomдля генерации разных исходов. - Сохранение прогресса: сериализация
GameStateв JSON и запись в файл. - Локализация: вынос текстов в ресурсы или словари.
Пример сохранения:
public static void SaveGame(GameState state, string path)
{
var json = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json);
}
Преимущества подхода
- Читаемость: каждая сцена — самостоятельный класс.
- Тестируемость: можно писать unit-тесты для
GetChoices. - Гибкость: логика выбора полностью отделима от вывода.
- Поддерживаемость: изменения в одной сцене не влияют на другие.
Текстовая игра на C# — это не просто развлечение, а учебный проект, демонстрирующий фундаментальные принципы разработки: инкапсуляцию, абстракцию, управление состоянием и проектирование взаимодействия с пользователем.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Проблема — Пользователи должны иметь возможность регистрироваться, входить в систему и получать доступ к персонализированному контенту или функционалу. Простой консольный чат на C — это учебное приложение, демонстрирующее базовые принципы сетевого взаимодействия между клиентом и сервером с использованием сокетов. Перед началом работы обязательно изучите главу Turtle . Scratch — визуальная образовательная среда программирования, разработанная MIT Media Lab. Особенности реализации — set -euo pipefail — обязательная практика для production-скриптов; - shift $((OPTIND - 1)) корректно обрабатывает как script.sh -c ., так и script.sh . -c; - -C и… echo off rem — Отключает вывод каждой команды (как set -x в bash) rem — в начале первой строки подавляет вывод её самой Примечание — использует XML-документацию, встроенную в модули. В PowerShell 7+ справка по умолчанию загружается из интернета, если локальные файлы отсутствуют. Примечание — для большинства случаев достаточно , но оно не поддерживает функции и некоторые нестандартные объекты (например, до ES2024 — поддержка есть, но не во всех средах выполнения, например,… ✅ Такой подход даёт полную типобезопасность без и без / . Подходит для лёгких сценариев или когда внешние зависимости нежелательны. ✅ Работает, если связь или гарантируется единственность. ⚠️ Для продакшена рекомендуются Jackson ( ) или Gson (более производительные и типобезопасные). удобен для прототипирования. Генератор случайных паролей — это утилита, создающая строки с заданными криптографическими свойствами — длина, наличие заглавных и строчных букв, цифр, специальных символов.Готовые решения
Простой консольный чат на CSharp
Примеры фигур Turtle на Python
Примеры скриптов Scratch
Примеры скриптов в Linux
Примеры команд в cmd
Примеры команд в PowerShell
Примеры решений в JavaScript
Примеры решений в TypeScript
Примеры запросов в SQL
Примеры решений в Java
Генератор случайных паролей на CSharp