Некоторые проекты, несмотря на свою кажущуюся простоту, дают нам возможность углубиться в важные основные концепции програdммирования и отточить базовые навыки. Создание симулятора игральных костей — отличный способ прокачать начинающим навыки работы с вводом данных, генерацией случайных чисел и манипуляцией строками в Python. Итак, какой план?
Что мы будем делать?
Мы разработаем приложение с TUI (Text-Based User Interface), которое позволит пользователю указать количество шестигранных костей для броска (от 1 до 6). После каждого броска приложение будет генерировать и отображать на экране ASCII-визуализацию результата броска.
Вот как оно будет работать:
- Пользователь увидит запрос на ввод количества костей.
- Произойдёт проверка того, является ли введенное значение целым числом от 1 до 6.
- Будет сгенерирован случайный результат для каждой кости.
- Результаты броска будут представлены в виде ASCII-визуализации, имитирующего игральные кости.
Какие навыки освоим?
Создание такого небольшого приложения позволит закрепить ряд базовых, но важных концепций Python:
- Работа с пользовательским вводом: мы будем получать данные от пользователя через командную строку.
- Валидация данных: проверим корректность введенных пользователем данных.
- Случайные числа: познакомитесь с модулем random и функцией `randint()`.
- Создание функций: структурируем свой код с помощью функций.
- Работа со строками: вспомним несколько полезных строковых методов.
- Рефакторинг код: на основе принципа единой ответственности (SPR) улучшим структуру кода.
К тому же, это приложение можно легко расширить — например, добавить поддержку игральных костей с разным количеством граней или историю бросков. Но давайте не забегать вперед — сначала разберем основы.
Что нам понадобится перед стартом?
Чтобы комфортно следовать этому туториалу, нужно знать некоторые базовые вещи:
- Как запускать скрипты из командной строки.
- Переменные и операторы сравнения.
- Основы работы с модулями (import).
- Типы данных: строки, числа, списки, словари.
- Условные конструкции (`if`-`else`) и циклы (`for`).
- Работа с функциями `input()` и `print()` .
Не волнуйтесь, если в каком-то из этих пунктов не уверены! Этот туториал — как раз хорошая возможность подтянуть свои знания.
Реализуем наш проект
Шаг 1: Создание текстового интерфейса
Наше приложение будет работать в терминале, поэтому первым делом настроим взаимодействие с пользователем. Мы попросим указать, сколько костей надо бросить (от 1 до 6), и убедимся, что введённые данные корректны.
Открываем любимый текстовый редактор или IDE и создаём файл `dice.py`. Пишем следующий код:
Открываем любимый текстовый редактор или IDE и создаём файл `dice.py`. Пишем следующий код:
num_dice_input = input("Сколько кубиков бросить? [1-6] ")
Эта строка выведет запрос ввода и сохранит ответ пользователя в переменную `num_dice_input` как строку. Но нам нужен не просто текст, а число, да еще и в пределах от 1 до 6. Значит, пора заняться валидацией.
Теперь напишем функцию `parse_input()`, которая преобразует строку в число и проверит, что оно подходит под условие:
def parse_input(input_string):
"""Возвращает `input_string` как целое число от 1 до 6.
Проверяет, является ли input_string целым числом между 1 и 6.
Если да, возвращает целое число. В противном случае сообщает
пользователю о необходимости ввести корректное число и завершает программу.
"""
if input_string.strip() in {"1", "2", "3", "4", "5", "6"}:
return int(input_string)
else:
print("Пожалуйста, введите число от 1 до 6.")
raise SystemExit(1)
Эта функция принимает строку, удаляет лишние пробелы с помощью `.strip()` и проверяет, входит ли значение в допустимый диапазон `{"1", "2", "3", "4", "5", "6"}`. Если ввод корректен, функция преобразует его в целое число и возвращает. В противном случае выводится сообщение об ошибке, и программа завершается.
Посмотрим, что получилось:
num_dice_input = input("Сколько костей бросить? [1-6] ")
num_dice = parse_input(num_dice_input)
print(f"Бросили костей: {num_dice}.")
Запускаем этот код, вводим что-нибудь и проверяем, что валидация работает как надо.
Шаг 2: Симуляция броска костей
Теперь, когда мы знаем, сколько костей нужно бросить, пора добавить немного случайности. Для этого воспользуемся модулем `random`. Создадим функцию `roll_dice()`, которая симулирует бросок заданного количества шестигранных кубиков:
import random
def roll_dice(num_dice):
"""Возвращает список целых чисел длиной num_dice.
Каждое целое число в возвращаемом списке представляет собой
случайное число от 1 до 6 включительно.
"""
roll_results = []
for _ in range(num_dice):
roll = random.randint(1, 6)
roll_results.append(roll)
return roll_results
Здесь мы сначала импортируем модуль `random`, который предоставляет функции для работы со случайными числами. Функция `roll_dice()` принимает количество костей `num_dice` в качестве аргумента. Внутри функции создается пустой список `roll_results`. Затем с помощью цикла `for` мы генерируем случайное целое число от 1 до 6 (включительно) для каждой кости, используя `random.randint(1, 6)`, и добавляем результат в список. Символ подчеркивания (`_`) в цикле `for` обычно используется, когда переменная цикла не используется внутри тела цикла. В конце функция возвращает список с результатами бросков.
Добавим это в наш код:
num_dice_input = input("Сколько костей бросить? [1-6] ")
num_dice = parse_input(num_dice_input)
roll_results = roll_dice(num_dice)
print(roll_results)
Запускаем скрипт и видим список чисел, например `[4, 2, 6]`. Но пока скучноват. Давайте добавим ASCII!
Шаг 3: Рисуем игральные кости с помощью ASCII
Теперь самое интересное — визуализация результатов броска! Сначала создадим словарь с ASCII-представлениями граней:
DICE_ART = {
1: (
"┌─────────┐",
"│ │",
"│ ● │",
"│ │",
"└─────────┘",
),
2: (
"┌─────────┐",
"│ ● │",
"│ │",
"│ ● │",
"└─────────┘",
),
3: (
"┌─────────┐",
"│ ● │",
"│ ● │",
"│ ● │",
"└─────────┘",
),
4: (
"┌─────────┐",
"│ ● ● │",
"│ │",
"│ ● ● │",
"└─────────┘",
),
5: (
"┌─────────┐",
"│ ● ● │",
"│ ● │",
"│ ● ● │",
"└─────────┘",
),
6: (
"┌─────────┐",
"│ ● ● │",
"│ ● ● │",
"│ ● ● │",
"└─────────┘",
),
}
Здесь мы создаем словарь `DICE_ART`, где ключами являются числа от 1 до 6 (значения на гранях кости), а значениями — кортежи строк, представляющие ASCII-изображение соответствующей грани. Теперь напишем функцию для генерации визуализации:
def generate_dice_faces_diagram(dice_values):
"""Возвращает ASCII-диаграмму граней костей из dice_values.
Возвращаемая строка содержит ASCII-представление каждой кости.
"""
dice_faces = [DICE_ART[value] for value in dice_values]
diagram_rows = []
for row_index in range(5):
row_components = [face[row_index] for face in dice_faces]
diagram_rows.append(" ".join(row_components))
width = len(diagram_rows[0])
diagram_header = " РЕЗУЛЬТАТЫ ".center(width, "~")
dice_faces_diagram = "\n".join([diagram_header] + diagram_rows)
return dice_faces_diagram
Функция принимает список результатов бросков `dice_values`. Она использует списковое включение для создания списка `dice_faces`, где каждый элемент — это ASCII-изображение соответствующей грани кости из `DICE_ART`. Затем функция проходит по каждой строке (от 0 до 4) и объединяет строки соответствующих граней костей с помощью пробела. В конце добавляется заголовок "РЕЗУЛЬТАТЫ", центрированный с помощью метода `.center()`, и все строки объединяются символом переноса строки `\n` для формирования вывода.
Теперь после получения и проверки ввода, броска костей и генерации результата мы просто отобразим результат при помощи `print()`:
dice_face_diagram = generate_dice_faces_diagram(roll_results)
print(f"\n{dice_face_diagram}")
Получим что-то такое:
Сколько костей бросить? [1-6] 6
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ РЕЗУЛЬТАТЫ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ ● │ │ ● ● │ │ ● ● │ │ ● │ │ ● ● │ │ ● │
│ │ │ ● ● │ │ ● ● │ │ │ │ ● ● │ │ ● │
│ ● │ │ ● ● │ │ ● ● │ │ ● │ │ ● ● │ │ ● │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
Шаг 4: Рефакторим код
Давайте обратим внимание на функцию `generate_dice_faces_diagram()` и заметим, что она решает несколько разных задач. Это нарушает принцип единственной ответственности, который гласит, что каждая функция, класс или модуль должны делать только одно дело.
Давайте проведём рефакторинг, разбив функцию на несколько вспомогательных. Например, можно создать функцию `_get_dice_faces()` для получения списка ASCII-изображений граней и функцию `_generate_diagram_rows()` для формирования строк диаграммы.
def _get_dice_faces(dice_values):
"""Возвращает список ASCII-представлений для значений костей."""
return [DICE_ART[value] for value in dice_values]
def _generate_diagram_rows(dice_faces):
"""Генерирует строки диаграммы из ASCII-представлений костей."""
dice_faces_rows = []
for row_idx in range(5):
row_components = [die[row_idx] for die in dice_faces]
row_string = " ".join(row_components)
dice_faces_rows.append(row_string)
return dice_faces_rows
def generate_dice_faces_diagram(dice_values):
"""Возвращает ASCII-диаграмму граней костей из dice_values.
Возвращаемая строка содержит ASCII-представление каждой кости.
"""
dice_faces = _get_dice_faces(dice_values)
dice_faces_rows = _generate_diagram_rows(dice_faces)
width = len(dice_faces_rows[0])
diagram_header = " РЕЗУЛЬТАТЫ ".center(width, "~")
return "\n".join([diagram_header] + dice_faces_rows)
Теперь каждая функция имеет свою конкретную ответственность:
Символ подчеркивания (_) в начале имени функции указывает, что эти функции предназначены для внутреннего использования и не должны вызываться напрямую извне.
- `_get_dice_faces()` получает ASCII-представления для конкретной грани;
- `_generate_dice_faces_rows()` формирует строки визуализации;
- `generate_dice_faces_diagram()` вызывает предыдущие и добавляет заголовок
Символ подчеркивания (_) в начале имени функции указывает, что эти функции предназначены для внутреннего использования и не должны вызываться напрямую извне.
Полная реализация кода
Вот полный код нашего симулятора игральных костей:
import random
DICE_ART = {
1: (
"┌─────────┐",
"│ │",
"│ ● │",
"│ │",
"└─────────┘",
),
2: (
"┌─────────┐",
"│ ● │",
"│ │",
"│ ● │",
"└─────────┘",
),
3: (
"┌─────────┐",
"│ ● │",
"│ ● │",
"│ ● │",
"└─────────┘",
),
4: (
"┌─────────┐",
"│ ● ● │",
"│ │",
"│ ● ● │",
"└─────────┘",
),
5: (
"┌─────────┐",
"│ ● ● │",
"│ ● │",
"│ ● ● │",
"└─────────┘",
),
6: (
"┌─────────┐",
"│ ● ● │",
"│ ● ● │",
"│ ● ● │",
"└─────────┘",
),
}
def parse_input(input_string):
"""Возвращает input_string как целое число от 1 до 6.
Проверяет, является ли input_string целым числом между 1 и 6.
Если да, возвращает целое число. В противном случае сообщает
пользователю о необходимости ввести корректное число и завершает программу.
"""
if input_string.strip() in {"1", "2", "3", "4", "5", "6"}:
return int(input_string)
else:
print("Пожалуйста, введите число от 1 до 6.")
raise SystemExit(1)
def roll_dice(num_dice):
"""Возвращает список целых чисел длиной num_dice.
Каждое целое число в возвращаемом списке представляет собой
случайное число от 1 до 6 включительно.
"""
roll_results = []
for _ in range(num_dice):
roll = random.randint(1, 6)
roll_results.append(roll)
return roll_results
def _get_dice_faces(dice_values):
"""Возвращает список ASCII-представлений для значений костей."""
return [DICE_ART[value] for value in dice_values]
def _generate_diagram_rows(dice_faces):
"""Генерирует строки диаграммы из ASCII-представлений костей."""
dice_faces_rows = []
for row_idx in range(5):
row_components = [die[row_idx] for die in dice_faces]
row_string = " ".join(row_components)
dice_faces_rows.append(row_string)
return dice_faces_rows
def generate_dice_faces_diagram(dice_values):
"""Возвращает ASCII-диаграмму граней костей из dice_values.
Возвращаемая строка содержит ASCII-представление каждой кости.
"""
dice_faces = _get_dice_faces(dice_values)
dice_faces_rows = _generate_diagram_rows(dice_faces)
width = len(dice_faces_rows[0])
diagram_header = " РЕЗУЛЬТАТЫ ".center(width, "~")
return "\n".join([diagram_header] + dice_faces_rows)
num_dice_input = input("Сколько костей бросить? [1-6] ")
num_dice = parse_input(num_dice_input)
roll_results = roll_dice(num_dice)
dice_face_diagram = generate_dice_faces_diagram(roll_results)
print(f"\n{dice_face_diagram}")
Что можно улучшить?
Созданный нами симулятор игральных костей — отличная основа для дальнейших экспериментов и улучшений. Вот несколько идей для развития проекта:
- Поддержка произвольного количества костей. При большом количестве костей можно отображать их в несколько строк.
- Поддержка костей с разным количеством граней. Симуляция костей с любым количеством граней, не только шестигранных, потребует создания новых ASCII-представлений и изменения логики симуляции.
- Добавление статистики. Можно добавить функционал для анализа результатов бросков, например, подсчет суммы, определение комбинаций (как в покере с костями).
- Создание интерактивной игры. Можно добавить соревновательный режим с другим пользователем.
- Добавление цвета. Цветные элементы в представление результата можно добавить, например, при помощи библиотеки colorama.
Этот проект, несмотря на свою относительную простоту, позволяет закрепить основы языка и может служить отличной основой для более сложных приложений.