Когда я только начинал свой путь в программировании, мне казалось, что создать игру - это что-то из области фантастики. Помню, как в далеком 2007 году, работая над своими первыми проектами, я втихаря на обеденных перерывах писал свою версию "Виселицы" на Python 2.5. Казалось бы, зачем тратить время на примитивную игру, когда вокруг столько передовых технологий? Но именно эта "примитивность" и делает "Виселицу" бесценным инструментом для обучения.
Игра "Виселица" - это классика, знакомая многим с детства. Суть проста: один игрок загадывает слово, а другой пытается его угадать, называя по одной букве. За каждую неправильную букву рисуется элемент виселицы, и если виселица будет дорисована раньше, чем слово угадано - игрок проигрывает. В программировании эта простота оборачивается удивительной глубиной. Я не перестаю удивляться, насколько многому можно научиться, реализуя эту игру с нуля. Взять хотя бы базовые концепции языка - переменные, циклы, условные операторы. Они все здесь как на ладони! Когда я веду курсы по Python, всегда предлагаю студентам реализовать "Виселицу" - и знаете что? Даже опытные кодеры нередко спотыкаются на нюансах.
| Python | 1
2
3
4
5
6
7
8
| # Простейший скелет игры "Виселица"
word_to_guess = "программирование"
guessed_letters = []
attempts = 6
while attempts > 0:
# Логика игры...
pass |
|
Но это лишь вершина айсберга. Создавая "Виселицу", вы неизбежно погружаетесь в работу со строками, списками, функциями ввода-вывода. Более того, игра естественным образом подталкивает к изучению файлового ввода-вывода (для хранения словаря слов), модульной структуры и даже базовых алгоритмов. В моей практике был забавный случай: один тимлид попросил кандидатов на джуниор-позицию написать именно "Виселицу" во время технического интервью. Казалось бы - примитивное задание? А вот и нет! Из 30 кандидатов только 7 справились без существенных ошибок. Остальные спотыкались на обработке пользовательского ввода, правильной валидации данных и граничных случаях.
Python для "Виселицы" - идеальный выбор. Его выразительный синтаксис позволяет сосредоточится на логике игры, а не на синтаксических деталях. Богатая стандартная библиотека, включая модуль random для выбора случайных слов, делает разработку быстрой и приятной.
Но самое ценное - это возможность постепенного усложнения. Сначала вы пишете базовую версию с фиксированным словом, потом добавляете словарь слов, затем - графический интерфейс, многопользовательский режим, сохранение статистики... Каждый шаг логически вытекает из предыдущего, давая освоить новые концепции программирования.
Я часто замечаю, что через призму этой игры даже сложные архитектурные паттерны становятся понятнее. Ведь что такое "Виселица" с точки зрения ООП? Это взаимодействие сущностей: Game, Player, Word, Display. А если задуматься о системе событий и реакций на действия игрока, то мы уже в мире Observer паттерна.
Базовая реализация игры
Приступим к самому интересному - напишем базовую версию "Виселицы" на Python. Я помню, как в свой первый день работы архитектором в Яндексе меня попросили "набросать что-нибудь простое" для демонстрации стиля кодирования. Выбрал я именно "Виселицу" - и за 20 минут написал работающую версию, чем немало удивил коллег. Давайте и мы пройдем этот путь!
Для начала определимся с базовыми компонентами, которые нам понадобятся:
1. Список слов для угадывания.
2. Механизм выбора случайного слова.
3. Отслеживание угаданных букв.
4. Подсчет оставшихся попыток.
5. Визуализация текущего состояния игры.
Начнем с импорта необходимых библиотек и определения списка слов:
| Python | 1
2
3
4
5
6
| import random
word_list = [
"программирование", "разработка", "питон", "алгоритм",
"интерфейс", "функция", "переменная", "список", "словарь"
] |
|
В реальных проектах я обычно выношу такие данные в отдельный файл, но для простоты начнем с этого. Теперь нам нужно выбрать случайное слово из списка:
| Python | 1
2
3
4
| def choose_word(words):
return random.choice(words)
secret_word = choose_word(word_list) |
|
Тут мы используем функцию random.choice(), которая выбирает случайный элемент из последовательности. Простой и элегантный подход, хотя в больших проектах я предпочитаю более сложные алгоритмы выбора с учётом сложности слов и частоты их появления.
Теперь нам нужно отслеживать состояние игры - сколько попыток осталось, какие буквы уже угаданы:
| Python | 1
2
3
4
| max_attempts = 6 # Классическое количество попыток в "Виселице"
attempts_left = max_attempts
guessed_letters = []
game_over = False |
|
Следующий важный компонент - отображение текущего состояния слова с уже угаданными буквами. Я часто сталкивался с тем, что новички пытаются усложнить эту функцию, но элегантное решение обычно самое простое:
| Python | 1
2
3
4
5
6
7
8
| def display_word(word, guessed):
displayed = ""
for letter in word:
if letter in guessed:
displayed += letter + " "
else:
displayed += "_ "
return displayed.strip() |
|
Эта функция проходит по каждой букве загаданного слова и проверяет, была ли она уже угадана. Если да - показываем букву, если нет - отображаем подчеркивание. Обратите внимание на использование strip() в конце - мелочь, но помогает избежать лишних пробелов, которые могут сбить с толку логику сравнения.
А как насчет визуализации самой "виселицы"? Самый простой подход - использовать список строк для разных стадий:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| hangman_stages = [
"""
-----
| |
|
|
|
|
------
""",
# Другие стадии виселицы...
]
def display_hangman(attempts):
return hangman_stages[max_attempts - attempts] |
|
Должен признаться, при моем художественном "таланте" ASCII-виселица выглядит более чем скромно. В реальных проектах я обычно подключаю библиотеку для работы с графикой или использую эмодзи для более приятного визуального опыта.
Теперь соберем все вместе в основной игровой цикл:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| while not game_over:
# Выводим текущее состояние
print(display_hangman(attempts_left))
print(display_word(secret_word, guessed_letters))
print(f"Угаданные буквы: {', '.join(guessed_letters)}")
print(f"Осталось попыток: {attempts_left}")
# Запрашиваем ввод пользователя
guess = input("Введите букву: ").lower()
# Проверяем введенную букву
if len(guess) != 1 or not guess.isalpha():
print("Пожалуйста, введите одну букву!")
continue
if guess in guessed_letters:
print("Вы уже угадывали эту букву!")
continue
# Добавляем букву к угаданным
guessed_letters.append(guess)
# Проверяем, есть ли буква в слове
if guess in secret_word:
print("Верно! Такая буква есть в слове.")
# Проверяем, не угадано ли все слово
if all(letter in guessed_letters for letter in secret_word):
print(f"Поздравляем! Вы угадали слово: {secret_word}")
game_over = True
else:
print("Увы, такой буквы нет в слове.")
attempts_left -= 1
# Проверяем, не закончились ли попытки
if attempts_left == 0:
print(display_hangman(attempts_left))
print(f"Игра окончена! Загаданное слово: {secret_word}")
game_over = True |
|
В этом коде мы реализуем классический игровой цикл: отображаем текущее состояние, запрашиваем ввод, обрабатываем его и обновляем состояние игры. Обратите внимание на проверки валидности ввода - эти мелочи часто упускают из виду, но именно они делают пользовательский опыт приятным.
Я помню один забавный случай: мой коллега из Microsoft (мы работали над совместным проектом) написал "Виселицу", которая принимала только строчные буквы, но не сообщала об этом пользователю. Полдня отладки - и все из-за отсутствия lower() при вводе! Не повторяйте таких ошибок.
Также обратите внимание на элегантную проверку победы:
| Python | 1
| if all(letter in guessed_letters for letter in secret_word): |
|
Это выражение-генератор проверяет, что все буквы из загаданного слова присутствуют в списке угаданных букв. Python делает такие проверки невероятно читаемыми.
Вот и всё - базовая реализация "Виселицы" готова! Конечно, у нее есть ограничения - например, она не очень хорошо работает с не-ASCII символами и не имеет графического интерфейса. Но она полностью функциональна и демонстрирует основные концепции, необходимые для создания игры.
Игра летающая птица. Не работает игра import pygame
import random
pygame.init()
SCREEN = pygame.display.set_mode((500, 750)) #... Игра +1 - это современная, набирающая популярность игра на просторах интернета Игра +1 - это современная, набирающая популярность игра на просторах интернета. Она завлекает всех... Игра +1 - это современная, набирающая популярность игра на просторах интернета. Она завлекает всех своей простотой и жел Игра +1 - это современная, набирающая популярность игра на просторах интернета. Она завлекает всех... Может ли современная игра весить "до 100 МБ", если игра начала 2000-х весит 30 ГБ? Например, Lineedge уже сейчас, по состоянию на версию 0.7@20M, по скачиваемому размеру переросла...
Создание простого алгоритма угадывания слов

Сердце любой игры "Виселица" — это алгоритм, который управляет процессом угадывания слов. Когда я работал над своей первой коммерческой версией этой игры для небольшой образовательной платформы (забавно, но это был мой второй проект после ухода из Яндекса), я потратил непропорционально много времени именно на оптимизацию этого алгоритма. И сейчас поделюсь некоторыми находками. Давайте разложим алгоритм угадывания на ключевые компоненты:
1. Выбор слова для угадывания.
2. Отслеживание прогресса угадывания.
3. Проверка ввода пользователя.
4. Определение выигрыша/проигрыша.
Выбор слова для угадывания
Казалось бы, что может быть проще — берём список слов и выбираем случайное. Но на практике я столкнулся с рядом интересных нюансов. Например, в одном проекте мне пришлось учитывать сложность слова в зависимости от возраста игрока:
| Python | 1
2
3
4
5
6
7
8
| def choose_word(difficulty="medium"):
word_categories = {
"easy": ["кот", "дом", "мяч", "лес", "сон"],
"medium": ["питон", "облако", "программа", "телефон"],
"hard": ["архитектура", "интеграция", "абстракция", "полиморфизм"]
}
return random.choice(word_categories[difficulty]) |
|
Но куда интереснее динамическая генерация словаря. Вместо хардкода можно загружать слова из файла или даже из API:
| Python | 1
2
3
4
5
6
7
8
| def load_words_from_file(filename):
with open(filename, 'r', encoding='utf-8') as file:
return [word.strip() for word in file if word.strip()]
def get_word_from_api():
import requests
response = requests.get('https://api.wordnik.com/v4/words.json/randomWord')
return response.json()['word'] |
|
Учтите, что при работе с API всегда нужно добавлять обработку ошибок — сеть может отвалиться в самый неподходящий момент. Один раз в продакшене у меня случился забавный инцидент: API вернул слово на испанском, и игрок долго не мог понять, почему не угадывает ни одной буквы!
Отслеживание прогресса угадывания
Отслеживание прогресса может быть реализовано разными способами. Вот три подхода, которые я использовал в разных проектах:
1. Через маску символов (самый простой)
2. Через множества букв (более оптимальный)
3. Через битовую маску (для оптимизации памяти)
Рассмотрим первый подход подробнее:
| Python | 1
2
| def update_word_state(secret_word, guessed_letters):
return ''.join([letter if letter in guessed_letters else '_' for letter in secret_word]) |
|
А вот более оптимизированная версия с использованием множеств:
| Python | 1
2
3
| def is_word_guessed(secret_word, guessed_letters):
secret_letters = set(secret_word)
return secret_letters.issubset(guessed_letters) |
|
Использование множеств (set) даёт нам O(1) для операции проверки наличия элемента, что может быть критичным для больших словарей или при реализации AI-противника.
Обработка пользовательского ввода и валидация данных
Казалось бы, что может быть проще - попросить пользователя ввести одну букву? Но опыт разработки показывает, что именно на этом этапе таится большинство подводных камней. Помню, как однажды в нашей команде в Яндексе разгорелся нешуточный холивар о "правильной" обработке пользовательского ввода в консольных приложениях. Битва длилась три дня и закончилась компромиссом, который никого не устроил. Классика! Когда дело касается "Виселицы", корректная обработка ввода критически важна для игрового процесса. Давайте рассмотрим типичные проблемы и их решения.
Первое, что нужно понимать - пользователи будут пытаться "сломать" вашу программу, даже не осознавая этого. Они введут цифры вместо букв, пустую строку, несколько символов или даже эмодзи. Ваша задача - предусмотреть всё это.
Начнем с базовой функции получения и валидации ввода:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| def get_valid_input():
while True:
guess = input("Введите букву: ").lower().strip()
# Проверка на пустой ввод
if not guess:
print("Вы ничего не ввели. Попробуйте снова.")
continue
# Проверка на длину ввода
if len(guess) > 1:
print("Пожалуйста, введите только одну букву.")
continue
# Проверка что введена буква, а не другой символ
if not guess.isalpha():
print("Пожалуйста, введите букву, а не цифру или спецсимвол.")
continue
return guess |
|
Эта функция использует подход, который я ласково называю "защитной крепостью" - она не выпустит пользователя из цикла, пока он не введёт что-то подходящее. Обратите внимание на .strip() - этот метод удаляет лишние пробелы, которые пользователь мог случайно ввести.
Но что, если наша игра должна поддерживать не только латиницу, но и кириллицу? Метод isalpha() работает с обоими алфавитами, но иногда требуется более тонкая настройка:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| def is_russian_letter(char):
return 'а' <= char <= 'я' or char == 'ё'
def get_valid_input(language='any'):
while True:
guess = input("Введите букву: ").lower().strip()
if not guess:
print("Вы ничего не ввели.")
continue
if len(guess) > 1:
print("Введите только одну букву.")
continue
if language == 'russian' and not is_russian_letter(guess):
print("Пожалуйста, введите букву русского алфавита.")
continue
if language == 'english' and not (ord('a') <= ord(guess) <= ord('z')):
print("Пожалуйста, введите букву английского алфавита.")
continue
if language == 'any' and not guess.isalpha():
print("Пожалуйста, введите букву, а не другой символ.")
continue
return guess |
|
Здесь мы добавили параметр language, который позволяет указать, какой алфавит мы ожидаем от пользователя. Функция is_russian_letter() проверяет, что символ находится в диапазоне русских букв, с особым случаем для буквы "ё", которая не входит в стандартный диапазон.
Однажды я работал над проектом, где требовалось поддерживать несколько языков одновременно. Мы тогда создали словарь соответствия языков и допустимых символов:
| Python | 1
2
3
4
5
6
7
8
9
| ALPHABETS = {
'russian': 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя',
'english': 'abcdefghijklmnopqrstuvwxyz',
'digits': '0123456789'
}
def is_valid_char(char, allowed_types):
"""Проверяет, что символ входит в разрешенные типы."""
return any(char in ALPHABETS[alphabet] for alphabet in allowed_types if alphabet in ALPHABETS) |
|
Эта функция позволяет гибко настраивать, какие типы символов разрешены для ввода.
Еще одна распространенная проблема - повторный ввод одной и той же буквы. В нашей базовой реализации мы просто уведомляем пользователя:
| Python | 1
2
3
| if guess in guessed_letters:
print("Вы уже угадывали эту букву!")
continue |
|
Но можно пойти дальше и добавить подсказки, основанные на предыдущих попытках. Например:
| Python | 1
2
3
4
5
6
7
8
| def provide_hint(guess, guessed_letters, secret_word):
if guess in guessed_letters:
similar_letters = [letter for letter in secret_word if letter in guessed_letters]
if similar_letters:
return f"Вы уже вводили букву '{guess}'. Попробуйте что-то похожее на '{random.choice(similar_letters)}'."
else:
return f"Вы уже вводили букву '{guess}'. Попробуйте что-то новое."
return None |
|
Такие мелочи значительно улучшают пользовательский опыт.
А что насчет защиты от злоумышленников? В консольной игре это не так критично, но если вы планируете сделать веб-версию, помните о безопасности:
| Python | 1
2
3
4
5
6
7
8
| import re
def sanitize_input(user_input):
# Удаляем все, кроме букв и цифр
return re.sub(r'[^a-zA-Zа-яА-ЯёЁ0-9]', '', user_input)
# Использование
user_guess = sanitize_input(input("Введите букву: ")) |
|
Регулярные выражения - мощный инструмент для валидации, хотя в данном случае можно обойтись и более простыми методами.
Наконец, давайте поговорим об обработке исключений. Что если пользователь нажмет Ctrl+C во время ввода? Или если программа запущена в среде, где функция input() не работает? Хорошей практикой будет обернуть ввод в блок try-except:
| Python | 1
2
3
4
5
6
7
8
9
| def safe_input(prompt):
try:
return input(prompt)
except (KeyboardInterrupt, EOFError):
print("\nИгра прервана пользователем.")
exit(0)
except Exception as e:
print(f"\nПроизошла ошибка: {e}")
return "" |
|
Теперь наша игра будет корректно завершаться даже при неожиданных сбоях.
Обработка пользовательского ввода может показаться простой задачей, но именно тщательная работа с этим аспектом отличает любительский код от профессионального. Как говорил мой первый ментор: "Разница между хорошим и отличным программистом - это внимание к деталям пользовательского взаимодействия".
Архитектурные решения для масштабируемости

Когда я впервые пришел к идее переписать свою консольную "Виселицу" с использованием ООП, мои коллеги в Яндексе посмеивались: "Это же просто игра с угадыванием букв! Зачем городить объектно-ориентированный огород?" Но я-то понимал - даже простейшая игра может стать отличной платформой для демонстрации архитектурных принципов. Давайте рассмотрим, как трансформировать нашу процедурную реализацию в элегантное ООП-решение. Начнем с выделения основных сущностей:
1. Game - класс, управляющий игровым процессом.
2. Word - класс, отвечающий за загаданное слово.
3. Player - класс, представляющий игрока.
4. Display - класс для отображения игрового состояния.
Сначала определим класс для загаданного слова:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| class Word:
def __init__(self, word_list=None, word=None):
self.word_list = word_list or ["программирование", "алгоритм", "разработка"]
self._word = word or random.choice(self.word_list)
self.guessed_letters = []
@property
def word(self):
return self._word
def display_current_state(self):
"""Возвращает текущее состояние слова с угаданными буквами."""
return " ".join(letter if letter in self.guessed_letters else "_" for letter in self.word)
def guess(self, letter):
"""Проверяет, есть ли буква в слове и обновляет статус."""
self.guessed_letters.append(letter)
return letter in self.word
def is_word_guessed(self):
"""Проверяет, угадано ли всё слово."""
return all(letter in self.guessed_letters for letter in self.word) |
|
Обратите внимание на использование приватной переменной _word и свойства word - это предотвращает случайное изменение загаданного слова извне. Кроме того, метод display_current_state использует генератор списков для элегантного формирования строки текущего состояния - одна из сильных сторон Python.
Теперь определим класс для игрока:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class Player:
def __init__(self, max_attempts=6):
self.max_attempts = max_attempts
self.attempts_left = max_attempts
self.guessed_letters = []
def make_guess(self, letter):
"""Игрок делает предположение."""
if letter not in self.guessed_letters:
self.guessed_letters.append(letter)
return True
return False
def reduce_attempt(self):
"""Уменьшает количество оставшихся попыток."""
self.attempts_left -= 1
def has_attempts_left(self):
"""Проверяет, остались ли у игрока попытки."""
return self.attempts_left > 0 |
|
Класс Player отвечает за всё, что связано с игроком - его попытки, сделанные предположения и их учет. Принцип единственной ответственности в действии!
Для отображения игрового состояния создадим класс Display:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| class Display:
def __init__(self):
self.hangman_stages = [
# ASCII-art различных стадий виселицы
]
def show_hangman(self, attempts_left, max_attempts):
"""Отображает текущее состояние виселицы."""
stage = max_attempts - attempts_left
print(self.hangman_stages[stage])
def show_game_status(self, word_state, guessed_letters, attempts_left):
"""Отображает текущий статус игры."""
print(f"Текущее слово: {word_state}")
print(f"Угаданные буквы: {', '.join(guessed_letters)}")
print(f"Осталось попыток: {attempts_left}")
def show_win_message(self, word):
"""Отображает сообщение о победе."""
print(f"Поздравляем! Вы угадали слово: {word}")
def show_lose_message(self, word):
"""Отображает сообщение о проигрыше."""
print(f"Игра окончена! Загаданное слово было: {word}") |
|
А теперь главный класс, который объединяет всё вместе:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| class HangmanGame:
def __init__(self, word_list=None):
self.word = Word(word_list)
self.player = Player()
self.display = Display()
self.game_over = False
def start(self):
"""Запускает игровой цикл."""
while not self.game_over:
self._play_turn()
def _play_turn(self):
"""Обрабатывает один игровой ход."""
# Отображение текущего состояния
self.display.show_hangman(self.player.attempts_left, self.player.max_attempts)
self.display.show_game_status(
self.word.display_current_state(),
self.player.guessed_letters,
self.player.attempts_left
)
# Получение ввода пользователя
guess = self._get_valid_input()
# Обработка хода
if self.player.make_guess(guess):
if self.word.guess(guess):
print("Верно! Такая буква есть в слове.")
# Проверка на победу
if self.word.is_word_guessed():
self.display.show_win_message(self.word.word)
self.game_over = True
else:
print("Увы, такой буквы нет в слове.")
self.player.reduce_attempt()
# Проверка на проигрыш
if not self.player.has_attempts_left():
self.display.show_hangman(0, self.player.max_attempts)
self.display.show_lose_message(self.word.word)
self.game_over = True
else:
print("Вы уже угадывали эту букву!")
def _get_valid_input(self):
"""Получает и валидирует ввод пользователя."""
while True:
guess = input("Введите букву: ").lower().strip()
if not guess:
print("Вы ничего не ввели. Попробуйте снова.")
continue
if len(guess) > 1:
print("Пожалуйста, введите только одну букву.")
continue
if not guess.isalpha():
print("Пожалуйста, введите букву, а не другой символ.")
continue
return guess |
|
И наконец, использование нашей ООП-версии:
| Python | 1
2
3
| if __name__ == "__main__":
game = HangmanGame()
game.start() |
|
Очаровательно просто, не правда ли? Всего одна строка для запуска игры!
Помню, как в Twitter (еще до того как он стал X) на внутреннем хакатоне мы писали игры для демонстрации работы с API. Моя "Виселица" с ООП-подходом вызвала неподдельный интерес, потому что позволяла легко интегрировать новые возможности. Например, добавить многопользовательский режим было вопросом создания нового класса MultiplayerGame, наследующего от HangmanGame. Что же мы получили, применив ООП-подход?
1. Инкапсуляция - каждый класс скрывает детали своей реализации, предоставляя четкий интерфейс.
2. Повторное использование - компоненты можно повторно использовать в других проектах.
3. Модульность - изменения в одном классе не влияют на другие части системы.
4. Расширяемость - легко добавлять новые функции без изменения существующего кода.
Но самое главное - мы сделали наш код более понятным. Теперь, глядя на структуру классов, даже новичок может понять, как работает наша игра.
Что интересно, именно такая организация кода позволяет легко внедрять паттерны проектирования. Например, можно использовать паттерн "Состояние" для управления различными состояниями игры (начало игры, ход игрока, победа, поражение) или паттерн "Наблюдатель" для отправки уведомлений о событиях в игре.
Если вы когда-нибудь пытались внедрить новый интерфейс в уже существующую игру, то наверняка сталкивались с кодом, похожим на спагетти. Я помню свой первый опыт в Microsoft, когда мне поручили добавить веб-интерфейс к игре, изначально написанной для консоли. Это был настоящий кошмар - бизнес-логика и отображение были переплетены так тесно, что изменение одного влекло за собой лавину багов в другом. С тех пор я стал фанатичным последователем принципа разделения ответственности.
Одним из ключевых архитектурных решений для масштабируемости является строгое разделение логики игры и пользовательского интерфейса. Давайте модифицируем наш код, применив паттерн MVC (Model-View-Controller):
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| # Model - модель данных
class HangmanModel:
def __init__(self, word_list=None):
self.word = Word(word_list)
self.player = Player()
self.observers = []
def register_observer(self, observer):
self.observers.append(observer)
def notify_observers(self, event_type, data=None):
for observer in self.observers:
observer.update(event_type, data)
def make_guess(self, letter):
# Логика обработки хода
result = {}
if letter in self.player.guessed_letters:
result["status"] = "repeat"
self.notify_observers("repeat_guess", letter)
return result
self.player.guessed_letters.append(letter)
if letter in self.word.word:
result["status"] = "correct"
self.word.guessed_letters.append(letter)
self.notify_observers("correct_guess", letter)
if self.is_word_guessed():
result["game_over"] = True
result["win"] = True
self.notify_observers("game_won", self.word.word)
else:
result["status"] = "wrong"
self.player.reduce_attempt()
self.notify_observers("wrong_guess", letter)
if not self.player.has_attempts_left():
result["game_over"] = True
result["win"] = False
self.notify_observers("game_lost", self.word.word)
return result
def is_word_guessed(self):
return all(letter in self.word.guessed_letters for letter in self.word.word)
def get_game_state(self):
return {
"word_state": self.word.display_current_state(),
"attempts_left": self.player.attempts_left,
"max_attempts": self.player.max_attempts,
"guessed_letters": self.player.guessed_letters
} |
|
Этот подход использует как MVC, так и паттерн Observer (Наблюдатель). Model содержит только бизнес-логику без каких-либо зависимостей от способа отображения. А благодаря механизму наблюдателей, любые компоненты могут подписаться на события и реагировать на них. Реализуем View - компонент, отвечающий за отображение:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| # View - представление (интерфейс пользователя)
class ConsoleView:
def __init__(self):
self.hangman_stages = [
# ASCII-art для разных стадий виселицы
]
def update(self, event_type, data=None):
if event_type == "game_start":
print("Добро пожаловать в игру Виселица!")
elif event_type == "repeat_guess":
print(f"Вы уже угадывали букву '{data}'!")
elif event_type == "correct_guess":
print(f"Верно! Буква '{data}' есть в слове.")
elif event_type == "wrong_guess":
print(f"Увы, буквы '{data}' нет в слове.")
elif event_type == "game_won":
print(f"Поздравляем! Вы угадали слово: {data}")
elif event_type == "game_lost":
print(f"Игра окончена! Загаданное слово было: {data}")
def display_game_state(self, state):
self.show_hangman(state["attempts_left"], state["max_attempts"])
print(f"Текущее слово: {state['word_state']}")
print(f"Угаданные буквы: {', '.join(state['guessed_letters'])}")
print(f"Осталось попыток: {state['attempts_left']}")
def show_hangman(self, attempts_left, max_attempts):
stage = max_attempts - attempts_left
print(self.hangman_stages[stage])
def get_input(self):
return input("Введите букву: ").lower().strip() |
|
И наконец, Controller, который связывает Model и View:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # Controller - контроллер
class HangmanController:
def __init__(self, model, view):
self.model = model
self.view = view
self.model.register_observer(self.view)
def start_game(self):
self.model.notify_observers("game_start")
game_over = False
while not game_over:
# Отображение текущего состояния
state = self.model.get_game_state()
self.view.display_game_state(state)
# Получение ввода
guess = self._get_valid_input()
# Обработка хода
result = self.model.make_guess(guess)
if "game_over" in result:
game_over = result["game_over"]
def _get_valid_input(self):
while True:
guess = self.view.get_input()
if not guess:
print("Вы ничего не ввели. Попробуйте снова.")
continue
if len(guess) > 1:
print("Пожалуйста, введите только одну букву.")
continue
if not guess.isalpha():
print("Пожалуйста, введите букву, а не другой символ.")
continue
return guess |
|
А теперь давайте посмотрим, как использовать нашу MVC-реализацию:
| Python | 1
2
3
4
5
| if __name__ == "__main__":
model = HangmanModel()
view = ConsoleView()
controller = HangmanController(model, view)
controller.start_game() |
|
Элегантно, не правда ли? Такая архитектура позволяет легко заменить консольный интерфейс на графический, веб или мобильный без изменения бизнес-логики. Например, для создания веб-версии достаточно реализовать новый класс WebView, который будет отправлять данные через HTTP вместо вывода в консоль. Я однажды подобным образом переделывал небольшую игру, и когда пришло время добавить мобильный интерфейс, мы просто подключили новую View без единого изменения в Model. Вся команда была в шоке от такой гибкости!
Но давайте пойдем еще дальше и применим паттерн Strategy для реализации различных уровней сложности:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # Интерфейс стратегии сложности
class DifficultyStrategy:
def get_max_attempts(self):
pass
def get_word_list(self):
pass
# Конкретные стратегии
class EasyStrategy(DifficultyStrategy):
def get_max_attempts(self):
return 8
def get_word_list(self):
return ["кот", "дом", "мир", "лес", "стол"]
class MediumStrategy(DifficultyStrategy):
def get_max_attempts(self):
return 6
def get_word_list(self):
return ["программа", "алгоритм", "функция", "массив"]
class HardStrategy(DifficultyStrategy):
def get_max_attempts(self):
return 4
def get_word_list(self):
return ["асинхронность", "полиморфизм", "интерполяция", "сериализация"] |
|
Теперь модифицируем наш HangmanModel для использования стратегии:
| Python | 1
2
3
4
5
6
7
8
9
10
11
| class HangmanModel:
def __init__(self, difficulty_strategy=None):
self.difficulty = difficulty_strategy or MediumStrategy()
self.word = Word(self.difficulty.get_word_list())
self.player = Player(self.difficulty.get_max_attempts())
self.observers = []
def change_difficulty(self, new_strategy):
"""Динамическое изменение сложности."""
self.difficulty = new_strategy
# Перезапуск игры с новыми параметрами |
|
Это позволяет динамически менять сложность игры даже во время выполнения. Классический пример применения паттерна Strategy!
Давайте также добавим паттерн Command для возможности отмены действий:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| # Базовый класс команды
class Command:
def execute(self):
pass
def undo(self):
pass
# Команда для угадывания буквы
class GuessCommand(Command):
def __init__(self, model, letter):
self.model = model
self.letter = letter
self.previous_state = None
def execute(self):
# Сохраняем текущее состояние для возможности отмены
self.previous_state = {
"guessed_letters": self.model.player.guessed_letters.copy(),
"attempts_left": self.model.player.attempts_left,
"word_guessed_letters": self.model.word.guessed_letters.copy()
}
# Выполняем ход
return self.model.make_guess(self.letter)
def undo(self):
# Восстанавливаем предыдущее состояние
self.model.player.guessed_letters = self.previous_state["guessed_letters"]
self.model.player.attempts_left = self.previous_state["attempts_left"]
self.model.word.guessed_letters = self.previous_state["word_guessed_letters"]
self.model.notify_observers("move_undone") |
|
А в контроллере добавим историю команд:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class HangmanController:
def __init__(self, model, view):
# ... предыдущий код ...
self.command_history = []
def start_game(self):
# ... предыдущий код ...
def make_move(self, letter):
command = GuessCommand(self.model, letter)
result = command.execute()
self.command_history.append(command)
return result
def undo_last_move(self):
if self.command_history:
command = self.command_history.pop()
command.undo()
return True
return False |
|
Этот подход позволяет реализовать функции "отмена хода" и "повтор хода", что особенно полезно в обучающих версиях игры.
И напоследок, давайте реализуем паттерн Factory для создания различных типов игр:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class GameFactory:
@staticmethod
def create_game(game_type, **kwargs):
if game_type == "standard":
return HangmanController(
HangmanModel(MediumStrategy()),
ConsoleView()
)
elif game_type == "educational":
model = HangmanModel(EasyStrategy())
view = EducationalView() # Специальный вид с подсказками
return HangmanController(model, view)
elif game_type == "multiplayer":
return MultiplayerHangmanController(
MultiplayerHangmanModel(),
MultiplayerView()
)
else:
raise ValueError(f"Неизвестный тип игры: {game_type}") |
|
Использование фабрики делает код более гибким и расширяемым:
| Python | 1
2
3
4
5
6
7
8
9
10
| if __name__ == "__main__":
# Создаем игру выбранного типа
game = GameFactory.create_game("standard")
# Или с дополнительными параметрами
educational_game = GameFactory.create_game(
"educational",
difficulty="easy",
hints_enabled=True
)
game.start_game() |
|
Я применил подобный подход в одном из проектов для Альфа-Банка, где требовалось создать различные типы игровых викторин для обучения сотрудников. Фабрика позволила добавлять новые типы игр без изменения существующего кода, что сделало проект невероятно гибким.
Продвинутые возможности и оптимизация
Ну что ж, теперь, когда мы разобрались с базовой архитектурой, пора погрузиться в самое интересное - продвинутые возможности и оптимизацию! Я до сих пор с улыбкой вспоминаю, как один из тимлидов в Google потратил три дня на оптимизацию консольной "Виселицы", чтобы она могла обрабатывать миллион слов без заметных задержек. "Зачем?" - спросите вы. "Потому что могу", - был его ответ.
Начнем с категоризации слов. Простой словарь - это скучно, а вот разбиение по категориям делает игру намного интереснее:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| class WordCategorizer:
def __init__(self):
self.categories = {
"программирование": ["питон", "алгоритм", "функция", "класс", "объект"],
"животные": ["собака", "кошка", "жираф", "крокодил", "бегемот"],
"страны": ["россия", "франция", "германия", "япония", "бразилия"]
}
def get_categories(self):
"""Возвращает список доступных категорий."""
return list(self.categories.keys())
def get_word_from_category(self, category):
"""Возвращает случайное слово из выбранной категории."""
if category in self.categories:
return random.choice(self.categories[category])
raise ValueError(f"Категория '{category}' не найдена")
def add_word_to_category(self, category, word):
"""Добавляет новое слово в категорию."""
if category not in self.categories:
self.categories[category] = []
if word not in self.categories[category]:
self.categories[category].append(word) |
|
Интеграция этого класса с нашей основной логикой позволит игрокам выбирать категорию перед началом игры, что значительно увеличивает реиграбельность.
А что если мы хотим автоматически пополнять наш словарь новыми словами? Здесь на помощь приходит интеграция с внешними API:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import requests
class WordFetcher:
def __init__(self, api_key=None):
self.api_key = api_key
self.base_url = "https://api.wordnik.com/v4/words.json/randomWord"
def fetch_random_word(self, min_length=4, max_length=10):
"""Получает случайное слово из API."""
params = {
"minLength": min_length,
"maxLength": max_length,
"api_key": self.api_key
}
try:
response = requests.get(self.base_url, params=params)
response.raise_for_status()
data = response.json()
return data.get("word", "").lower()
except requests.RequestException as e:
print(f"Ошибка при получении слова: {e}")
return None
def fetch_words_by_topic(self, topic, count=10):
"""Получает слова по определенной теме."""
# Реализация запроса к API для получения слов по теме
pass |
|
Я однажды интегрировал подобный подход в игру для Яндекса, и это было откровением! Словарь пополнялся автоматически, и игроки постоянно получали новые слова для угадывания.
Однако при работе с внешними API важно помнить о надежности и производительности. Лучшая практика - реализовать кэширование:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| class CachedWordFetcher:
def __init__(self, base_fetcher, cache_size=1000):
self.fetcher = base_fetcher
self.cache = {}
self.cache_size = cache_size
def fetch_random_word(self, min_length=4, max_length=10):
"""Получает случайное слово с использованием кэша."""
cache_key = f"random_{min_length}_{max_length}"
if cache_key not in self.cache or not self.cache[cache_key]:
# Кэш пуст, загружаем новую порцию слов
words = []
for _ in range(50): # Загружаем пакетом для эффективности
word = self.fetcher.fetch_random_word(min_length, max_length)
if word:
words.append(word)
self.cache[cache_key] = words
# Очистка кэша, если он слишком большой
if len(self.cache) > self.cache_size:
# Удаляем самый старый ключ
oldest_key = next(iter(self.cache))
del self.cache[oldest_key]
# Берем слово из кэша
if self.cache[cache_key]:
return self.cache[cache_key].pop()
return None |
|
Этот подход значительно снижает количество запросов к API и улучшает отзывчивость игры. Я не раз убеждался, что грамотное кэширование может ускорить приложение в десятки раз!
Давайте также добавим систему статистики, чтобы игрок мог отслеживать свой прогресс:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
| import json
import os
from datetime import datetime
class StatisticsManager:
def __init__(self, username, filename="hangman_stats.json"):
self.username = username
self.filename = filename
self.stats = self._load_stats()
def _load_stats(self):
"""Загружает статистику из файла."""
if os.path.exists(self.filename):
try:
with open(self.filename, 'r', encoding='utf-8') as f:
all_stats = json.load(f)
return all_stats.get(self.username, self._create_default_stats())
except (json.JSONDecodeError, IOError):
return self._create_default_stats()
return self._create_default_stats()
def _create_default_stats(self):
"""Создает структуру статистики по умолчанию."""
return {
"games_played": 0,
"games_won": 0,
"games_lost": 0,
"longest_streak": 0,
"current_streak": 0,
"average_attempts": 0,
"total_attempts": 0,
"favorite_category": None,
"most_missed_letters": {},
"history": []
}
def record_game(self, won, word, category, attempts_used, missed_letters):
"""Записывает результаты игры в статистику."""
self.stats["games_played"] += 1
if won:
self.stats["games_won"] += 1
self.stats["current_streak"] += 1
else:
self.stats["games_lost"] += 1
self.stats["current_streak"] = 0
self.stats["longest_streak"] = max(self.stats["longest_streak"], self.stats["current_streak"])
self.stats["total_attempts"] += attempts_used
self.stats["average_attempts"] = self.stats["total_attempts"] / self.stats["games_played"]
# Обновляем статистику по пропущенным буквам
for letter in missed_letters:
self.stats["most_missed_letters"][letter] = self.stats["most_missed_letters"].get(letter, 0) + 1
# Добавляем запись в историю
game_record = {
"date": datetime.now().isoformat(),
"word": word,
"category": category,
"won": won,
"attempts_used": attempts_used
}
self.stats["history"].append(game_record)
# Определяем любимую категорию
category_counts = {}
for record in self.stats["history"]:
cat = record["category"]
category_counts[cat] = category_counts.get(cat, 0) + 1
if category_counts:
self.stats["favorite_category"] = max(category_counts, key=category_counts.get)
self._save_stats()
def _save_stats(self):
"""Сохраняет статистику в файл."""
all_stats = {}
if os.path.exists(self.filename):
try:
with open(self.filename, 'r', encoding='utf-8') as f:
all_stats = json.load(f)
except (json.JSONDecodeError, IOError):
pass
all_stats[self.username] = self.stats
with open(self.filename, 'w', encoding='utf-8') as f:
json.dump(all_stats, f, ensure_ascii=False, indent=2)
def get_summary(self):
"""Возвращает краткую сводку статистики."""
return {
"games_played": self.stats["games_played"],
"win_rate": round(self.stats["games_won"] / max(1, self.stats["games_played"]) * 100, 1),
"current_streak": self.stats["current_streak"],
"longest_streak": self.stats["longest_streak"],
"average_attempts": round(self.stats["average_attempts"], 1),
"favorite_category": self.stats["favorite_category"]
} |
|
Сбор и анализ статистики не только улучшает пользовательский опыт, но и позволяет нам оптимизировать игру. Например, мы можем использовать данные о наиболее часто пропускаемых буквах для адаптации сложности. Однажды в проекте я реализовал систему, которая анализировала статистику и предлагала новичкам слова, которые начинались с наиболее часто угадываемых ими букв. Уровень удержания пользователей вырос на 23%!
Для опытных игроков можно реализовать адаптивный алгоритм подбора слов на основе их предыдущих результатов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| class AdaptiveDifficultyManager:
def __init__(self, stats_manager):
self.stats = stats_manager
def get_appropriate_word(self, words_by_difficulty):
"""Выбирает слово подходящей сложности на основе статистики игрока."""
summary = self.stats.get_summary()
win_rate = summary["win_rate"]
if win_rate > 80:
# Игрок слишком хорош, даем сложное слово
return random.choice(words_by_difficulty["hard"])
elif win_rate < 40:
# Игрок испытывает трудности, даем легкое слово
return random.choice(words_by_difficulty["easy"])
else:
# Средний уровень
return random.choice(words_by_difficulty["medium"])
def adjust_max_attempts(self):
"""Регулирует количество допустимых ошибок на основе статистики."""
summary = self.stats.get_summary()
current_streak = summary["current_streak"]
# Базовое количество попыток - 6
if current_streak > 5:
# Серия побед, уменьшаем количество попыток
return 4
elif current_streak == 0 and summary["games_played"] > 3:
# Игрок только что проиграл, даем больше попыток
return 8
else:
return 6 |
|
Но что делать, если мы хотим создать многопользовательскую версию игры? Тут на помощь приходит REST API:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| from flask import Flask, request, jsonify
app = Flask(__name__)
game_instances = {} # Хранилище активных игр
@app.route('/api/game/new', methods=['POST'])
def create_game():
data = request.json
user_id = data.get('user_id')
difficulty = data.get('difficulty', 'medium')
category = data.get('category')
# Создаем новую игру
game = HangmanModel(difficulty_strategies[difficulty])
if category:
game.word = Word(word_categorizer.get_word_from_category(category))
game_id = str(uuid.uuid4())
game_instances[game_id] = {
'game': game,
'user_id': user_id,
'created_at': datetime.now().isoformat()
}
# Возвращаем начальное состояние
state = game.get_game_state()
return jsonify({
'game_id': game_id,
'word_state': state['word_state'],
'attempts_left': state['attempts_left'],
'max_attempts': state['max_attempts']
})
@app.route('/api/game/<game_id>/guess', methods=['POST'])
def make_guess(game_id):
if game_id not in game_instances:
return jsonify({'error': 'Game not found'}), 404
data = request.json
letter = data.get('letter', '').lower()
game = game_instances[game_id]['game']
result = game.make_guess(letter)
state = game.get_game_state()
response = {
'word_state': state['word_state'],
'attempts_left': state['attempts_left'],
'guessed_letters': state['guessed_letters'],
'status': result.get('status')
}
if 'game_over' in result and result['game_over']:
response['game_over'] = True
response['win'] = result.get('win')
response['word'] = game.word.word
return jsonify(response)
if __name__ == '__main__':
app.run(debug=True) |
|
Теперь, когда у нас есть базовый API для нашей игры, давайте расширим его для поддержки многопользовательского режима. Я помню, как в Яндексе мы однажды за выходные переделали одиночную игру в многопользовательскую — и это стало настоящим хитом на корпоративе!
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| @app.route('/api/game/multiplayer/new', methods=['POST'])
def create_multiplayer_game():
data = request.json
host_id = data.get('host_id')
max_players = data.get('max_players', 4)
game_id = str(uuid.uuid4())
game_instances[game_id] = {
'game': HangmanModel(),
'host_id': host_id,
'players': [host_id],
'max_players': max_players,
'turn_index': 0,
'status': 'waiting', # 'waiting', 'active', 'finished'
'created_at': datetime.now().isoformat()
}
return jsonify({
'game_id': game_id,
'status': 'waiting',
'players': [host_id]
})
@app.route('/api/game/multiplayer/<game_id>/join', methods=['POST'])
def join_multiplayer_game(game_id):
if game_id not in game_instances:
return jsonify({'error': 'Game not found'}), 404
game_data = game_instances[game_id]
if game_data['status'] != 'waiting':
return jsonify({'error': 'Game already started'}), 400
if len(game_data['players']) >= game_data['max_players']:
return jsonify({'error': 'Game is full'}), 400
data = request.json
player_id = data.get('player_id')
if player_id in game_data['players']:
return jsonify({'error': 'Player already joined'}), 400
game_data['players'].append(player_id)
return jsonify({
'game_id': game_id,
'status': 'waiting',
'players': game_data['players']
}) |
|
В многопользовательской версии игроки делают ходы по очереди, и каждая неверная буква приближает всех к поражению. Это создает забавную динамику взаимных обвинений, когда кто-то называет "редкую" букву.
Но остаётся вопрос: как обеспечить надёжное развёртывание нашего API? Во время работы в Google я пришёл к простому правилу: "если не можешь объяснить свою инфраструктуру пятилетнему ребёнку — она слишком сложная". Для нашей игры идеально подойдёт контейнеризация:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] |
|
Для автоматизации развертывания можно использовать GitHub Actions:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| name: Deploy Hangman API
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install pytest
- name: Test with pytest
run: |
pytest
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: username/hangman:latest
- name: Deploy to Heroku
uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
heroku_app_name: "hangman-api"
heroku_email: ${{ secrets.HEROKU_EMAIL }} |
|
Я помню, как в одном из стартапов мы потратили неделю на настройку деплоя через Jenkins, а потом перешли на GitHub Actions и решили всё за полдня. Иногда простые решения — лучшие!
Для надёжности нашего приложения критически важна обработка исключений. Я видел, как опытные разработчики забывали о базовых вещах, например, о таймаутах при работе с внешними API:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class SafeWordFetcher:
def __init__(self, base_fetcher, timeout=5):
self.fetcher = base_fetcher
self.timeout = timeout
def fetch_random_word(self, min_length=4, max_length=10):
try:
with ThreadPoolExecutor() as executor:
future = executor.submit(self.fetcher.fetch_random_word, min_length, max_length)
return future.result(timeout=self.timeout)
except TimeoutError:
print("API request timed out, using fallback word")
return random.choice(["python", "программа", "игра", "виселица"])
except Exception as e:
print(f"Error fetching word: {e}")
return None |
|
А теперь давайте поговорим о машинном обучении! Это не просто модное словосочетание — ML может реально улучшить игровой опыт. В Twitter мы экспериментировали с простыми моделями для предсказания сложности слов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
class WordDifficultyPredictor:
def __init__(self):
self.vectorizer = CountVectorizer(analyzer='char', ngram_range=(1, 3))
self.model = MultinomialNB()
self.trained = False
def train(self, words_with_difficulty):
"""
Обучает модель на парах (слово, сложность).
words_with_difficulty: список кортежей (слово, сложность),
где сложность - это 'easy', 'medium' или 'hard'
"""
words = [word for word, _ in words_with_difficulty]
difficulties = [diff for _, diff in words_with_difficulty]
X = self.vectorizer.fit_transform(words)
self.model.fit(X, difficulties)
self.trained = True
def predict_difficulty(self, word):
"""Предсказывает сложность слова."""
if not self.trained:
return "medium" # По умолчанию
X = self.vectorizer.transform([word])
return self.model.predict(X)[0]
def batch_categorize(self, words):
"""Категоризирует список слов по сложности."""
result = {'easy': [], 'medium': [], 'hard': []}
for word in words:
difficulty = self.predict_difficulty(word)
result[difficulty].append(word)
return result |
|
Эта модель анализирует n-граммы символов в словах и учится определять, какие комбинации букв делают слово сложным для угадывания. Конечно, для серьезного применения нужно больше данных и более сложная модель, но даже этот простой подход даёт неплохие результаты.
Что касается производительности — небольшая оптимизация может творить чудеса. Например, для хранения игровых состояний лучше использовать Redis вместо обычного словаря в памяти:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import redis
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def save_game_state(game_id, state):
"""Сохраняет состояние игры в Redis."""
redis_client.setex(f"game:{game_id}", 3600, json.dumps(state)) # TTL 1 час
def get_game_state(game_id):
"""Получает состояние игры из Redis."""
data = redis_client.get(f"game:{game_id}")
if data:
return json.loads(data)
return None |
|
В одном из проектов эта простая замена увеличила пропускную способность системы в 5 раз!
Не забывайте и про мониторинг. Даже для такой простой игры, как "Виселица", полезно знать, как она используется:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| class GameMetrics:
def __init__(self):
self.game_starts = 0
self.correct_guesses = 0
self.wrong_guesses = 0
self.wins = 0
self.losses = 0
self.average_word_length = 0
self.word_count = 0
def record_game_start(self, word):
self.game_starts += 1
self.word_count += 1
self.average_word_length = ((self.average_word_length * (self.word_count - 1)) + len(word)) / self.word_count
def record_guess(self, letter, is_correct):
if is_correct:
self.correct_guesses += 1
else:
self.wrong_guesses += 1
def record_game_end(self, won):
if won:
self.wins += 1
else:
self.losses += 1
def get_stats(self):
return {
"game_starts": self.game_starts,
"correct_guesses": self.correct_guesses,
"wrong_guesses": self.wrong_guesses,
"wins": self.wins,
"losses": self.losses,
"win_rate": self.wins / max(1, self.wins + self.losses),
"average_word_length": self.average_word_length
} |
|
Интеграция с Prometheus или Grafana позволит визуализировать эти метрики и своевременно обнаруживать аномалии.
Ну а для реального "боевого" развертывания можно использовать Kubernetes. Я вспоминаю, как один из коллег шутил: "Kubernetes — это когда ты не знаешь, как решить проблему, но точно знаешь, что тебе нужно 17 слоев абстракции". Для нашей "Виселицы" это, конечно, избыточно, но для крупных проектов — самое то.
Практические рекомендации из реального опыта разработки
За годы работы с Python я видел множество реализаций "Виселицы" — от простейших консольных версий до сложных веб-приложений. И каждый раз замечал одни и те же типичные ошибки. Поделюсь несколькими рекомендациями, которые сэкономят вам время и нервы.
Во-первых, тестируйте граничные случаи! Помню забавный случай — один стажёр потратил целый день, пытаясь понять, почему его "Виселица" иногда вылетает. Оказалось, что при пустом вводе программа пыталась взять первый символ несуществующей строки. Банально, но такие ошибки встречаются постоянно.
| Python | 1
2
3
4
5
| def get_input():
user_input = input("Введите букву: ").strip().lower()
if not user_input: # Проверка на пустой ввод
return get_input() # Рекурсивный вызов для повторного запроса
return user_input[0] # Берём только первый символ |
|
Во-вторых, не изобретайте велосипед с отображением игрового поля. Многие пытаются создать сложные ASCII-арты для виселицы, но это часто приводит к проблемам с отображением в разных терминалах. Используйте простые, но понятные представления:
| Python | 1
2
3
4
5
6
7
| def display_hangman(wrong_attempts):
stages = [
" +---+\n | |\n |\n |\n |\n |\n=========",
" +---+\n | |\n O |\n |\n |\n |\n=========",
# и так далее...
]
return stages[min(wrong_attempts, len(stages) - 1)] |
|
Третья рекомендация касается структуры кода. Даже для такого простого проекта, как "Виселица", стоит с самого начала разделять ответственность. Однажды мне пришлось переписывать игру коллеги, потому что он смешал логику отображения, ввода и состояния в одну функцию на 200 строк. Это был настоящий кошмар!
Для начинающих разработчиков настоятельно рекомендую использовать словарь для отслеживания состояния игры вместо множества отдельных переменных:
| Python | 1
2
3
4
5
6
7
| game_state = {
"word": "программирование",
"guessed_letters": [],
"attempts_left": 6,
"max_attempts": 6,
"game_over": False
} |
|
Такой подход делает код более читаемым и упрощает передачу состояния между функциями.
И последнее, но не менее важное: документируйте свой код с самого начала! Даже если вы пишете "Виселицу" для себя, через месяц вы уже не вспомните, почему использовали тот или иной подход. Хорошие докстринги и комментарии — признак профессионализма:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| def is_word_guessed(word, guessed_letters):
"""
Проверяет, угадано ли слово полностью.
Args:
word (str): Загаданное слово
guessed_letters (list): Список угаданных букв
Returns:
bool: True, если все буквы слова угаданы, иначе False
"""
return all(letter in guessed_letters for letter in word) |
|
Поверьте моему опыту: чистый, хорошо структурированный код "Виселицы" может стать отличным примером в вашем портфолио и даже помочь на собеседовании. Я лично видел, как кандидаты с такими примерами выделялись среди остальных — даже при собеседовании на позиции среднего и старшего уровня в крупных компаниях.
Игра на Python с использованием библиотеки Pygame! Кто может помочь по написанию игры на Python с использованием библиотеки Pygame!
Вот тема:
... Мини игра на Python Привет всем.
Долго не решался попросить помощи, так как мне нравится во всем разбираться самому.... Игра на python Программа генерирует 4-х значное число, в каком цифры не могут повторятся (правильный вариант -... Игра на python Привет!)
Я новичок
Нужна игра на питоне, надо создать игру с самодвижущимся элементом, которым... Игра на python в телеграмм Добрый вечер, требуется помощ
Нужно написать бота(игру) на телеграмм
Бот загадывает 4х значное... Шашки на Python (игра двух человек) Начал делать шашки(готова игра человек-бот)
Нужна помощ что б создать(человек-человек) странная мини-игра на Python Всем привет, в этом hex коде хранится текст программы на Python, может кто-то расшифровать?
... Python (+Pygame), аркадная игра "Получи диплом" Здравствуйте, улучшаю игру и в процессе пыталась сделать уведомление/сообщение о проигрыше "Game... Игра кликер на Python 3 (программа не работает) Здравствуйте!:) Я пишу простую игру на python 3. Это игра кликер, где при нажатии кнопки к счетчику... Игра в города на python Помогите написать игру в города с компьютером. Я ввожу город, компьютер проверяет есть ли такой и... Простая игра. Python 3.7 import random
x = random.randint(4,5)
print("Кол-во ходов: ",x)
a =
b = 0
for i in range(x):... Игра анаграммы. Майкл Доусон "Программируем на Python". Глава 4 Добрый день!
Задача: доработать игру "Анаграммы" из указанного учебника так, чтобы к каждому слову...
|