$ sudo teach IT
МОДУЛЬ 2 · УРОК 5

Практика: атакующие воины ⚔️

Пришло время писать настоящую программу! Мы создадим двух воинов, которые будут биться насмерть. По дороге разберём, как опытный программист думает перед тем, как написать первую строчку кода. Это навык важнее синтаксиса.

⏱ ~45 минут 🎓 Для новичков 🐍 Python 3 ⚔️ Пишем игру!

🎯 Что будем делать

Мы напишем маленькую боевую симуляцию. Два воина встречаются на арене и бьют друг друга по очереди, пока один из них не упадёт. В конце программа объявляет победителя.

📋 Требования к программе:

  • У каждого воина есть имя, запас здоровья (100 HP) и урон от удара
  • Воины бьют друг друга по очереди
  • Удар снижает здоровье противника
  • Бой продолжается, пока оба живы
  • Программа объявляет имя победителя

💡 Важная мысль: прежде чем писать код — нужно подумать. Опытный разработчик 20% времени тратит на проектирование и 80% на написание. Новичок делает наоборот — бросается кодить и потом переписывает всё заново. Давайте будем умнее.

🏗️ Шаг 1: Проектируем класс — думаем перед тем как кодить

Перед написанием кода задайте себе два вопроса: что объект знает? и что объект умеет делать?

Первый вопрос даст нам атрибуты, второй — методы.

🧠
Что воин ЗНАЕТ?
→ своё имя (чтобы объявить победителя)
→ своё здоровье (чтобы знать, жив ли он)
→ свой урон (сколько HP снимает с врага)
⚡
Что воин УМЕЕТ?
→ атаковать противника (снижать его HP)
→ проверить, жив ли он сам (HP > 0?)
→ представиться (вывести своё состояние)

Отлично! Теперь у нас есть чёткий план. Переводим его в структуру класса:

ПЛАН КЛАССА (псевдокод)
class Warrior:
    # Атрибуты (что знает)
    name — имя воина
    hp — здоровье (100)
    damage — урон за удар

    # Методы (что умеет)
    attack(target) — ударить цель
    is_alive() — проверить живой ли
    __str__() — красивый вывод

✍️ Шаг 2: Пишем __init__ — создаём воина

Начинаем с конструктора. Здесь мы определяем, что нужно знать при создании воина, а что задаётся по умолчанию.

Рассуждение: имя и урон у каждого воина своё — передаём при создании. Здоровье всегда начинается со 100 — задаём значение по умолчанию прямо в параметре.

class Warrior:
    def __init__(self, name, damage):
        self.name = name       # имя воина — у каждого своё
        self.hp = 100          # здоровье — всегда начинается с 50
        self.damage = damage   # урон за удар — у каждого свой

🤔 Почему hp не параметр? Потому что правила игры диктуют: все воины начинают с одинаковым здоровьем. Жёстко задав self.hp = 100, мы гарантируем это правило — никто не сможет случайно создать воина с 999 HP. Это называется инкапсуляция логики в классе.

⚔️ Шаг 3: Метод attack() — наносим удар

Теперь самое интересное. Метод attack принимает другой объект в качестве аргумента — цель удара. Именно здесь объекты начинают взаимодействовать друг с другом.

Рассуждение: когда воин атакует, он снимает своё значение урона (self.damage) с HP другого воина (target.hp). Значит нам нужен доступ к атрибуту hp цели.

class Warrior:
    def __init__(self, name, damage):
        self.name = name
        self.hp = 100
        self.damage = damage

    def attack(self, target):
        # target — это другой объект Warrior
        # Снимаем с его HP наш урон
        target.hp -= self.damage
        print(f"{self.name} атакует {target.name}! "
              f"У {target.name} осталось {target.hp} HP")

🔑 Ключевой момент: метод принимает другой объект как параметр. Внутри метода мы работаем с атрибутами этого объекта через target.hp. Так объекты взаимодействуют — один читает и изменяет данные другого. Именно в этом мощь ООП.

❤️ Шаг 4: Метод is_alive() — проверка живого

Нам нужно знать, продолжать бой или нет. Воин жив, пока его HP больше нуля. Вынесем эту логику в отдельный метод — тогда нигде в коде не будет разбросано warrior.hp > 0, вся логика в одном месте.

    def is_alive(self):
        return self.hp > 0
        # Возвращает True, если воин жив
        # Возвращает False, если HP стало 0 или меньше

Почему стоит сделать именно метод, а не каждый раз писать воин.hp > 0 напрямую? Представьте, что завтра правила изменятся и воин считается мёртвым при HP < 10 (умирает от ран). Вам придётся найти и поменять warrior.hp > 0 во всех местах кода. А с методом — одно изменение в одном месте.

🎓 Принцип: «Если что-то про объект нужно вычислить — сделай это методом объекта». Это называется инкапсуляция. Данные и логика работы с ними живут вместе, внутри класса.

🖨️ Шаг 5: Метод __str__() — красивый вывод

Python позволяет научить объект «превращаться» в строку — для этого есть специальный метод __str__. Когда вы пишете print(воин), Python вызывает именно его.

    def __str__(self):
        return f"⚔ {self.name} | HP: {self.hp} | Урон: {self.damage}"

Теперь print(warrior1) выдаст красивую строку вместо непонятного <__main__.Warrior object at 0x...>.

📘 Забегая вперёд: __str__ — один из множества «dunder»-методов (от double underscore — двойное подчёркивание). Они позволяют объектам вести себя как встроенные типы Python. Мы подробно разберём их все в Модуле 7. Пока просто используйте этот паттерн как есть.

🔄 Шаг 6: Цикл боя — объясняем логику

Класс готов. Теперь пишем основную логику боя. Нам нужен цикл, который продолжается, пока оба воина живы — и в каждой итерации они бьют друг друга по очереди.

Рассуждение о цикле: используем while, потому что не знаем заранее, сколько ударов понадобится. Условие: оба воина должны быть живы для продолжения — это and из двух проверок is_alive().

# Создаём воинов
warrior1 = Warrior("Артур", damage=20)
warrior2 = Warrior("Мордред", damage=15)

print("=== БОЙ НАЧИНАЕТСЯ ===")
print(warrior1)
print(warrior2)
print()

# Цикл боя: пока оба живы
while warrior1.is_alive() and warrior2.is_alive():
    warrior1.attack(warrior2)   # Первый бьёт второго

    # Проверяем сразу после удара — вдруг второй уже мёртв?
    # Если не проверить, второй успеет ответить даже мёртвым
    if warrior2.is_alive():
        warrior2.attack(warrior1)   # Второй бьёт первого

print()
print("=== БОЙ ЗАВЕРШЁН ===")

⚠️ Важная деталь: мы проверяем if warrior2.is_alive() внутри цикла, перед тем как дать ему ответный удар. Иначе «мёртвый» воин успеет нанести последний удар после гибели — что логически неправильно.

🏆 Шаг 7: Определяем победителя

После окончания цикла один из воинов мёртв. Как определить, кто победил? Проверяем, кто всё ещё жив:

if warrior1.is_alive():
    print(f"Победитель: {warrior1.name}! 🏆")
    print(f"Остаток HP: {warrior1.hp}")
else:
    print(f"Победитель: {warrior2.name}! 🏆")
    print(f"Остаток HP: {warrior2.hp}")

🤔 А что если оба умерли одновременно? В нашей механике это невозможно: первый бьёт второго, и мы сразу проверяем, жив ли второй, прежде чем дать ему ответить. Значит один всегда умирает раньше. Но в более сложных играх (яды, DoT-урон) такое бывает — там добавляют проверку на ничью.

📄 Полный код целиком

Собираем всё вместе. Посмотрите на финальный код и обратите внимание, насколько он читается как рассказ:

class Warrior:
    def __init__(self, name, damage):
        self.name = name
        self.hp = 100
        self.damage = damage

    def attack(self, target):
        target.hp -= self.damage
        print(f"{self.name} атакует {target.name}! "
              f"У {target.name} осталось {target.hp} HP")

    def is_alive(self):
        return self.hp > 0

    def __str__(self):
        return f"⚔ {self.name} | HP: {self.hp} | Урон: {self.damage}"


# --- Запускаем бой ---

warrior1 = Warrior("Артур", damage=20)
warrior2 = Warrior("Мордред", damage=15)

print("=== БОЙ НАЧИНАЕТСЯ ===")
print(warrior1)
print(warrior2)
print()

while warrior1.is_alive() and warrior2.is_alive():
    warrior1.attack(warrior2)
    if warrior2.is_alive():
        warrior2.attack(warrior1)

print()
print("=== БОЙ ЗАВЕРШЁН ===")

if warrior1.is_alive():
    print(f"Победитель: {warrior1.name}! 🏆")
    print(f"Остаток HP: {warrior1.hp}")
else:
    print(f"Победитель: {warrior2.name}! 🏆")
    print(f"Остаток HP: {warrior2.hp}")

💻 Пример вывода программы

Вот как будет выглядеть вывод при запуске. Обратите внимание: Артур наносит больше урона за удар (20 против 15), поэтому побеждает первым, хотя сам тоже получил немало:

=== БОЙ НАЧИНАЕТСЯ ===
⚔ Артур | HP: 100 | Урон: 20
⚔ Мордред | HP: 100 | Урон: 15

Артур атакует Мордреда! У Мордреда осталось 80 HP
Мордред атакует Артура! У Артура осталось 85 HP
Артур атакует Мордреда! У Мордреда осталось 60 HP
Мордред атакует Артура! У Артура осталось 70 HP
Артур атакует Мордреда! У Мордреда осталось 40 HP
Мордред атакует Артура! У Артура осталось 55 HP
Артур атакует Мордреда! У Мордреда осталось 20 HP
Мордред атакует Артура! У Артура осталось 40 HP
Артур атакует Мордреда! У Мордреда осталось 0 HP

=== БОЙ ЗАВЕРШЁН ===
Победитель: Артур! 🏆
Остаток HP: 40

🧪 Задание для самостоятельной работы: случайный урон

Сейчас каждый удар наносит ровно self.damage урона. В реальных играх урон случайный — это делает бой непредсказуемым и интересным. Ваша задача — добавить это.

Что нужно сделать:

  • Добавьте параметры min_damage и max_damage вместо одного damage
  • В методе attack генерируйте случайный урон в этом диапазоне
  • Выводите, сколько именно урона нанесено в этот раз

Подсказка — понадобится модуль random:

import random

# Случайное целое число от 10 до 25 (включительно):
damage = random.randint(10, 25)

Как должно выглядеть создание воина после изменений:

warrior1 = Warrior("Артур", min_damage=15, max_damage=25)
warrior2 = Warrior("Мордред", min_damage=10, max_damage=20)

Запустите бой несколько раз подряд — каждый раз результат будет другим. Именно так работают ролевые игры!

🎯 Что важно запомнить из этого урока

  • Проектируй перед кодом: «что объект знает» → атрибуты; «что умеет» → методы.
  • Метод может принимать другой объект как аргумент и работать с его атрибутами — так объекты взаимодействуют.
  • is_alive() — пример правила, которое лучше держать внутри класса, а не разбрасывать по коду.
  • __str__ учит объект красиво выводиться через print(). Подробности — в Модуле 7.
  • Проверяй состояние сразу после важного действия — не давай «мёртвым» объектам продолжать действовать.

🚀 В следующем модуле мы разберём наследование — один из главных принципов ООП. Создадим разные типы воинов (маг, лучник, рыцарь), не переписывая базовый класс с нуля.