Введение: Хватит писать спагетти-код
Вспомните свой первый текстовый квест или простую игру. Скорее всего, ваш код выглядел примерно так:
hero_name = "Arthur"
hero_hp = 100
hero_damage = 10
enemy_name = "Orc"
enemy_hp = 80
enemy_damage = 5
# Битва
enemy_hp -= hero_damage
hero_hp -= enemy_damage
Вроде работает, да? Но что случится, если вы захотите добавить второго врага? А если десятерых? Вам придется создавать переменные enemy2_hp, enemy3_hp... А если герой подберет щит, который снижает урон? Вам придется переписывать всю логику боя, добавляя кучу проверок if.
Очень быстро такой код превращается в «спагетти», в котором страшно что-то менять. Вы запутываетесь в переменных, ловите баги и теряете желание программировать.
Знакомая ситуация?
Именно здесь на сцену выходит ООП (Объектно-Ориентированное Программирование).
Многие новички боятся этой аббревиатуры. Учебники пугают сложными словами: «полиморфизм», «инкапсуляция», «абстракция». А примеры часто скучные и оторванные от жизни: «Вот класс Машина, а вот класс Грузовик...».
Сегодня мы пойдем другим путем. Мы не будем строить абстрактные грузовики. Мы напишем движок для настоящей текстовой RPG-игры.
В этой статье мы:
Разберем, чем Класс отличается от Объекта (на примере чертежа и реального воина).
Упакуем хаос из переменных в аккуратные сущности.
Научим Паладина и Мага сражаться по-разному, используя Полиморфизм.
Сделаем код таким, чтобы добавление нового монстра занимало всего 3 строчки, а не переписывание половины программы.
Готовы превратить свои скрипты в серьезную архитектуру? Тогда открывайте IDE, сегодня мы создаем миры. ⚔️
Часть 1. Основы: Класс vs Объект (Чертеж vs Дом)
Прежде чем писать код, давайте разберемся с главными понятиями. Если вы поймете это сейчас, дальше всё пойдет как по маслу.
Представьте, что вы работаете на заводе по производству роботов.
Класс (Class) — это чертеж робота. В нем написано: «У робота должны быть имя, уровень заряда и серийный номер». Чертеж сам по себе ничего не делает. Он просто висит на стене.
Объект (Object) — это конкретный робот, собранный по этому чертежу. Его можно потрогать, включить, отправить на работу.
По одному чертежу (Классу) можно собрать тысячи разных роботов (Объектов). У одного будет имя «Бендер», у другого «R2D2», но структура у них одинаковая.
В нашей RPG-игре всё точно так же. Мы создадим Класс Character (шаблон), а потом наделаем из него конкретных Объектов (Героя, Злодея, Дракона).
Пишем наш первый класс
В Python класс создается ключевым словом class. По традиции названия классов пишут с БольшойБуквы (CamelCase).
class Character:
def __init__(self, name, health, damage):
self.name = name
self.health = health
self.damage = damage
def __str__(self):
return f"Имя: {self.name} | Здоровье: {self.health} | Урон: {self.damage}"
Выглядит просто, но тут есть два момента, которые пугают новичков: __init__ и self. Давайте разберем их "на пальцах".
1. Что такое __init__?
Это конструктор. Эта функция запускается автоматически ровно в тот момент, когда вы создаете нового персонажа. Её задача — «настроить» новорожденный объект: выдать ему имя, здоровье и оружие.
2. Магия self
Это самое непонятное слово для начинающих.
Представьте, что self — это бейджик с надписью «Я».
Когда мы пишем self.name = name, компьютер понимает: «Возьми имя, которое передали, и запиши его именно в МЕНЯ (в этот конкретный объект)».
Без self переменные просто повисли бы в воздухе и исчезли после выполнения функции. self приклеивает данные к конкретному персонажу.
Создаем героев (Инициализация)
Теперь, когда у нас есть чертеж, давайте создадим реальных бойцов.
# Создаем Героя (используем наш класс как фабрику)
player = Character("Artas", 100, 20)
# Создаем Монстра
enemy = Character("Orc", 80, 5)
Смотрите, что произошло:
Мы вызвали
Character(...).Python заглянул в чертеж.
Запустил
__init__.Для
playerон записал вself.nameстроку "Artas".Для
enemyон записал вself.nameстроку "Orc".
У нас появились две независимые сущности. Если мы ударим Орка, здоровье Артаса не уменьшится. Это разные «коробки» с данными.
Давайте проверим это кодом:
print("--- До битвы ---")
print(player)
print(enemy)
# Допустим, игрок нашел зелье лечения и изменил свои свойства
player.health += 50
# А орк нашел дубину потяжелее
enemy.damage += 10
print("\n--- После изменений ---")
print(player) # Здоровье изменилось только у игрока
print(enemy) # Урон изменился только у врага
Вывод в консоль:
--- До битвы ---
Имя: Artas | Здоровье: 100 | Урон: 20
Имя: Orc | Здоровье: 80 | Урон: 5
--- После изменений ---
Имя: Artas | Здоровье: 150 | Урон: 20
Имя: Orc | Здоровье: 80 | Урон: 15
Итог первой части:
Мы научились создавать структуру данных. Вместо кучи разрозненных переменных artas_hp, orc_hp, artas_dmg, у нас теперь есть аккуратные объекты player и enemy.
Часть 2. Методы: Учим персонажей действовать
В первой части мы создали «манекенов». У них есть имена и характеристики, но они стоят и ничего не делают. Если мы хотим экшена, нам нужны Методы.
В программировании всё как в лингвистике:
Атрибуты (переменные класса) — это Существительные (Здоровье, Имя).
Методы (функции класса) — это Глаголы (Атаковать, Получить урон, Лечиться).
Метод — это обычная функция, только она «живет» внутри класса и имеет доступ к self. Давайте научим наших героев драться!
1. Принимаем удар (take_damage)
Начнем с простого. Когда персонажа бьют, его здоровье должно уменьшаться.
Добавим этот метод в наш класс Character:
def take_damage(self, damage):
self.health -= damage
if self.health < 0:
self.health = 0 # Чтобы здоровье не ушло в минус
print(f"? {self.name} получил {damage} урона! Осталось HP: {self.health}")
Обратите внимание: мы не просто вычитаем число. Мы добавляем логику (проверку на отрицательное здоровье) и красивый вывод в консоль. Теперь эта логика спрятана внутри метода, и нам не нужно писать её каждый раз вручную.
2. Наносим удар (attack)
А теперь самое интересное: взаимодействие двух объектов.
Когда Герой атакует Монстра, мы должны передать Монстра внутрь метода атаки.
def attack(self, other):
print(f"⚔️ {self.name} атакует {other.name}!")
other.take_damage(self.damage)
Разберем, что тут происходит:
self— это тот, КТО бьет (Атакующий).other— это тот, КОГО бьют (Цель).other.take_damage(...)— мы «дергаем» метод цели, заставляя её получить урон.
Почему это круто?
Мы не пишем other.health -= self.damage напрямую. Мы вежливо «просим» врага принять урон. Это позволяет в будущем добавить броню или уклонение именно в метод take_damage, не ломая код атаки.
3. Полный код класса на данный момент
Давайте соберем всё вместе, чтобы вы могли скопировать и запустить.
class Character:
def __init__(self, name, health, damage):
self.name = name
self.health = health
self.damage = damage
def __str__(self):
return f"{self.name} (HP: {self.health}, Dmg: {self.damage})"
def take_damage(self, damage):
self.health -= damage
if self.health < 0:
self.health = 0
print(f" ? {self.name} получил {damage} урона. Текущее здоровье: {self.health}")
def attack(self, other):
print(f"⚔️ {self.name} наносит удар по {other.name}!")
other.take_damage(self.damage)
# Полезный метод, чтобы проверять, не закончилась ли игра
def is_alive(self):
return self.health > 0
Тест-драйв: Битва начинается!
Теперь наш код в файле main.py становится похож на сценарий фильма, а не на математическую задачу. Читать его — одно удовольствие.
# 1. Создаем бойцов
hero = Character("Korg", 100, 20)
monster = Character("Goblin", 50, 10)
# 2. Проверяем статус
print(hero)
print(monster)
print("-" * 20)
# 3. Обмен любезностями
hero.attack(monster) # Корг бьет Гоблина
monster.attack(hero) # Гоблин дает сдачи
hero.attack(monster) # Корг добивает
hero.attack(monster) # Контрольный удар (проверка на 0 HP)
print("-" * 20)
print(f"Жив ли гоблин? {monster.is_alive()}")
Что увидим в консоли:
Korg (HP: 100, Dmg: 20)
Goblin (HP: 50, Dmg: 10)
--------------------
⚔️ Korg наносит удар по Goblin!
? Goblin получил 20 урона. Текущее здоровье: 30
⚔️ Goblin наносит удар по Korg!
? Korg получил 10 урона. Текущее здоровье: 90
⚔️ Korg наносит удар по Goblin!
? Goblin получил 20 урона. Текущее здоровье: 10
⚔️ Korg наносит удар по Goblin!
? Goblin получил 20 урона. Текущее здоровье: 0
--------------------
Жив ли гоблин? False
Итог второй части:
Мы научили объекты «общаться». Один объект может вызывать методы другого. Это фундамент ООП.
Часть 3. Наследование: Не повторяй себя (DRY)
Представьте, что мы хотим расширить нашу игру. Нам нужен не просто безликий Character, а, например, Паладин, который умеет лечиться, и Ассасин, который бьет с шансом критического урона.
Плохой путь (Копипаста):
Скопировать код класса Character, вставить его два раза, переименовать в Paladin и Assassin и дописать нужные методы.
Почему это плохо? Потому что если вы найдете баг в методе take_damage, вам придется исправлять его в трех местах. А если классов будет 20? Это ад поддержки.
Хороший путь (Наследование):
Мы говорим Python: «Создай новый класс Hero, который будет точной копией Character, но с парой бонусов».
Класс Character становится Родителем (Базовым классом), а Hero — Потомком (Дочерним классом).
1. Создаем Героя
Синтаксис наследования очень прост. В скобках после имени нового класса мы указываем имя родителя.
# В скобках указываем, ОТ КОГО мы наследуемся
class Hero(Character):
pass
Слово pass означает «ничего не меняй». Даже с пустым телом класс Hero уже имеет методы __init__, attack и take_damage. Он получил их в наследство от папы.
Но мы хотим, чтобы Герой был особенным. Давайте дадим ему способность лечиться, которой не будет у обычных монстров.
class Hero(Character):
# Мы не пишем __init__, он наследуется автоматически!
def heal(self):
# Лечим на 20% от текущего здоровья, но не больше 100 (условно)
heal_amount = 10
self.health += heal_amount
print(f"✨ {self.name} применил магию и восстановил {heal_amount} HP. Теперь здоровья: {self.health}")
2. Создаем Монстра
Для врагов мы тоже создадим отдельный класс. Пока что он будет пустым, но это нужно для порядка в коде (семантики). В будущем мы сможем добавить туда уникальные фишки монстров (например, "рычание" или "дроп золота").
class Enemy(Character):
pass
3. Проверка связи
Теперь у нас есть иерархия:
-
Character(База: есть здоровье, может бить)Hero(Умеет всё, что База + умеет лечиться)Enemy(Умеет всё, что База)
Давайте посмотрим, как это работает на практике.
# Создаем персонажей через НОВЫЕ классы
player = Hero("Lancelot", 100, 20) # Использует __init__ от Character
monster = Enemy("Dragon", 200, 5) # Использует __init__ от Character
print("--- Битва началась ---")
# Герой атакует (метод взят у родителя)
player.attack(monster)
# Монстр атакует (метод взят у родителя)
monster.attack(player)
print("--- Ход героя ---")
# Герой решает подлечиться (УНИКАЛЬНЫЙ метод)
player.heal()
# А вот монстр так не может!
# monster.heal() # <--- Это вызовет ошибку: 'Enemy' object has no attribute 'heal'
Вывод в консоль:
--- Битва началась ---
⚔️ Lancelot наносит удар по Dragon!
? Dragon получил 20 урона. Текущее здоровье: 180
⚔️ Dragon наносит удар по Lancelot!
? Lancelot получил 5 урона. Текущее здоровье: 95
--- Ход героя ---
✨ Lancelot применил магию и восстановил 10 HP. Теперь здоровья: 105
В чем сила, брат?
Мы написали метод heal только один раз и только там, где он нужен (в Герое).
Если мы создадим 10 разных героев, все они смогут лечиться.
Монстры лечиться не смогут (у них нет этого метода).
Базовая механика боя (
attack,take_damage) лежит в одном месте (Character). Если мы захотим изменить формулу урона, мы поменяем её только там, и это автоматически применится и к Героям, и к Монстрам.
Итог третьей части:
Мы разделили бойцов на два лагеря. Код стал чище и логичнее.
Но есть нюанс: сейчас все герои бьют одинаково. А что, если мы хотим, чтобы Маг бил магией (игнорируя броню), а Воин мог нанести критический удар?
Часть 4. Полиморфизм: Одна команда — разный результат
Слово «Полиморфизм» звучит сложно, но на деле это самая крутая фишка ООП.
Переводится это как «Много форм».
Суть проста: у нас есть разные классы (Воин, Маг, Лучник). У всех них есть команда attack(). Но выполняют они её по-разному.
В прошлом уроке мы унаследовали метод attack от Character. Он работал скучно: просто передавал урон.
Теперь мы сделаем так:
Воин (Warrior) будет бить с шансом нанести двойной урон (Крит).
Маг (Mage) будет гарантированно наносить урон магией (с другим описанием).
Чтобы изменить поведение наследника, нам нужно просто переписать метод с тем же именем внутри нового класса. Это называется Переопределение метода (Method Overriding).
1. Создаем Воина (Механика крита)
Нам понадобится модуль random, чтобы шанс срабатывал не всегда.
import random
class Warrior(Hero):
def attack(self, other):
# 20% шанс нанести критический урон (х2)
if random.random() < 0.2:
crit_damage = self.damage * 2
print(f"⚔️ {self.name} делает ЯРОСТНЫЙ ВЫПАД! (Крит x2)")
other.take_damage(crit_damage)
else:
# Если крит не выпал, бьем как обычно
# super() обращается к родителю (Character) и берет его метод attack
super().attack(other)
Что здесь нового?
Мы добавили логику внутрь класса. Главной программе не нужно знать, как именно бьет воин. Она просто говорит «Атакуй!», а воин сам решает — крит это или обычный удар.
2. Создаем Мага (Магическая атака)
Маг не машет мечом, он кидает фаерболы. Ему не нужен метод super().attack(), мы напишем свою реализацию полностью с нуля.
class Mage(Hero):
def attack(self, other):
# Магия игнорирует обычную защиту (в будущем), и выглядит эпично
print(f"? {self.name} кастует Огненный Шар в {other.name}!")
other.take_damage(self.damage + 5) # Маг бьет чуть больнее базового урона
3. Магия Полиморфизма в действии
А теперь смотрите, ради чего мы всё это затеяли. Представьте, что у нас в игре есть отряд героев.
# Создаем отряд
conan = Warrior("Conan", 150, 15)
gandalf = Mage("Gandalf", 80, 20)
# Список бойцов
party = [conan, gandalf]
# Создаем грушу для битья
dummy_monster = Character("Dummy", 500, 0)
print("--- Атака отряда ---")
for hero in party:
# МАГИЯ ТУТ: Мы вызываем один и тот же метод attack()
# Но каждый герой делает это по-своему!
hero.attack(dummy_monster)
Вывод в консоль (может отличаться из-за рандома):
--- Атака отряда ---
⚔️ Conan делает ЯРОСТНЫЙ ВЫПАД! (Крит x2)
? Dummy получил 30 урона. Текущее здоровье: 470
? Gandalf кастует Огненный Шар в Dummy!
? Dummy получил 25 урона. Текущее здоровье: 445
Почему это гениально?
Представьте, что вы пишете главный цикл игры.
Без полиморфизма ваш код выглядел бы так (ужас):
# ПЛОХОЙ КОД (БЕЗ ООП)
if type(hero) == Warrior:
calculate_crit()
elif type(hero) == Mage:
cast_fireball()
elif type(hero) == Archer:
shoot_arrow()
# И так для каждого из 50 классов...
С полиморфизмом ваш код выглядит так всегда, сколько бы классов вы ни добавили:
# ХОРОШИЙ КОД
hero.attack(target)
Вы можете добавить в игру Друида, Паладина или Робота. Главный цикл игры даже не придется трогать. Новый класс сам знает, как ему атаковать. Это делает ваш код расширяемым.
Итог четвертой части:
Мы научили разные классы реагировать на одну и ту же команду по-своему. Теперь у нас есть настоящий «зоопарк» персонажей с уникальным поведением.
Часть 5. Собираем всё вместе: Game Loop
У нас есть чертежи (Классы), есть бойцы (Объекты) и есть правила боя (Методы). Теперь нам нужна Арена.
В программировании игр это называется Game Loop (Игровой цикл). Это бесконечный цикл while, который крутится до тех пор, пока кто-то не победит.
Благодаря тому, что мы спрятали всю сложную логику (урон, криты, проверки здоровья) внутрь классов, наш главный скрипт будет читаться как увлекательная книга.
Нам понадобится модуль time, чтобы добавлять задержку между ударами — так мы сможем успевать читать лог боя в консоли.
Код запуска игры (main.py)
import time
import random
# Представим, что классы Character, Hero, Warrior, Mage, Enemy описаны выше
# 1. СОЗДАНИЕ БОЙЦОВ
# Игрок выбирает класс (в нашем коде жестко зададим Воина)
player = Warrior("Conan", 120, 15)
# Генерируем врага
enemy = Character("Goblin", 100, 10)
print("--- ? БОЙ НАЧИНАЕТСЯ! ---")
print(player)
print(enemy)
print("-" * 30)
# 2. ИГРОВОЙ ЦИКЛ
# Битва идет, пока оба живы (используем метод is_alive, который мы писали ранее)
while player.is_alive() and enemy.is_alive():
time.sleep(1.5) # Пауза для драматизма
# --- Ход Игрока ---
# Нам не важно, Воин это или Маг. Метод attack() сам разберется, что делать.
player.attack(enemy)
# Проверка победы сразу после удара
if not enemy.is_alive():
print(f"\n? {player.name} ОДЕРЖАЛ ПОБЕДУ!")
break
# --- Ход Врага ---
enemy.attack(player)
# Проверка поражения
if not player.is_alive():
print(f"\n? {player.name} пал в бою... Game Over.")
break
# --- Дополнительная логика (Фишка ООП) ---
# Если герой сильно побит, он может попытаться полечиться
# Проверяем: если здоровье меньше 30% и повезло с рандомом
if player.health < 40 and random.random() < 0.3:
print(" ? Герой пытается найти зелье...")
player.heal() # Этот метод есть только у Hero и его наследников!
print("-" * 30)
Что мы увидим в консоли?
Запустив этот код, вы увидите настоящий текстовый блокбастер. Обратите внимание, как срабатывает логика критов (из класса Warrior) и логика лечения (из класса Hero), хотя в главном цикле while мы написали всего пару строчек.
--- ? БОЙ НАЧИНАЕТСЯ! ---
Conan (HP: 120, Dmg: 15)
Goblin (HP: 100, Dmg: 10)
------------------------------
⚔️ Conan делает ЯРОСТНЫЙ ВЫПАД! (Крит x2)
? Goblin получил 30 урона. Осталось HP: 70
⚔️ Goblin атакует Conan!
? Conan получил 10 урона. Осталось HP: 110
(пауза 1.5 сек)
⚔️ Conan атакует Goblin!
? Goblin получил 15 урона. Осталось HP: 55
⚔️ Goblin атакует Conan!
? Conan получил 10 урона. Осталось HP: 100
... (битва продолжается) ...
? Conan ОДЕРЖАЛ ПОБЕДУ!
------------------------------
Итог
Поздравляю! Вы только что написали рабочий движок для RPG.
Посмотрите на цикл while. Он занимает всего 15 строк. Даже если мы добавим в игру 50 видов монстров, магию, инвентарь и уровни сложности — этот цикл почти не изменится.
В этом и есть главная сила ООП: мы прячем сложность внутрь классов, оставляя внешний код чистым и понятным.
Теперь всё в ваших руках. Добавляйте новые классы, придумывайте монстров и создавайте свои миры. Основа у вас уже есть! ?
Часть 6. Инкапсуляция (Коротко, на будущее)
Мы уже написали отличную игру, но в нашем коде есть одна опасная дыра.
Смотрите:
hero.health = -1000
hero.damage = "Очень много"
Ничто не мешает нам (или другому программисту в нашей команде) случайно присвоить здоровью отрицательное значение или вообще записать туда текст. Python не будет ругаться, но игра сломается в самый неподходящий момент.
Здесь на помощь приходит Инкапсуляция.
Это принцип, который гласит: «Не лезь внутрь механизма, используй кнопки».
Представьте смартфон. Вы нажимаете кнопки громкости (Методы), чтобы изменить звук (Данные). Вы не разбираете корпус и не паяете провода каждый раз, когда хотите сделать потише. Корпус защищает внутренности от ваших шаловливых рук.
Как это делается в Python?
В других языках (Java, C++) есть жесткие запреты (private). В Python всё держится на «джентльменском соглашении».
Если мы хотим сказать: «Эту переменную трогать нельзя, она для внутреннего пользования», мы ставим перед её названием нижнее подчеркивание _.
Давайте слегка улучшим наш класс Character:
class Character:
def __init__(self, name, health, damage):
self.name = name
self._health = health # <--- Внимание на черточку!
self._max_health = health # Запомним максимум
self.damage = damage
# Мы создаем "кнопку" для изменения здоровья
def take_damage(self, damage):
self._health -= damage
# Внутри метода мы можем добавить ПРОВЕРКИ (Валидацию)
if self._health < 0:
self._health = 0
def heal(self):
self._health += 10
# Не даем вылечиться выше максимума!
if self._health > self._max_health:
self._health = self._max_health
В чем разница?
Плохой способ:
hero.health += 1000— Мы влезли «в кишки» объекта. Герой может получить 10000 здоровья, хотя его максимум 100. Баланс игры сломан.Правильный способ (Инкапсуляция):
hero.heal()— Мы нажали «кнопку». Метод сам проверил: «Ага, лечимся на 10, но не выше максимума».
Суть инкапсуляции одной фразой:
Мы закрываем прямой доступ к данным (_health) и заставляем работать с ними только через методы (take_damage, heal), в которых прописаны правила безопасности.
Теперь вы знаете, что если видите в чужом коде переменную с подчеркиванием (например, _id или _password), — трогать её руками не стоит. Используйте методы! ?️
Домашнее задание: Прокачай свои навыки
Чтобы теория не выветрилась из головы, попробуйте решить эти задачи. Я спрятал условия под спойлеры, чтобы не занимать много места.
Задача 1. Класс «Лучник» (Риск и награда)
Создайте класс Archer, который наследуется от Hero.
Механика: Лучник стреляет издалека и наносит огромный урон, но иногда промахивается.
1. Переопределите метод attack().
2. Добавьте проверку точности: с шансом 20% стрела летит "в молоко" (ур��н 0, в консоль пишется "Лучник промахнулся!").
3. Если попал — наносит обычный урон.
Подсказка: Используйте random.random().
Задача 2. Паладин и Броня (Работа с take_damage)
Мы научились менять атаку, теперь давайте изменим получение урона.
Создайте класс Paladin. В __init__ добавьте ему атрибут self.armor (например, 5 единиц).
Переопределите метод take_damage(self, damage).
Логика:
Когда Паладин получает урон, из него сначала вычитается броня. Если броня больше урона, здоровье не меняется.
Пример: Урон 15, Броня 5 -> Отнимаем 10 HP.
Пример: Урон 3, Броня 5 -> Отнимаем 0 HP (не лечимся от ударов!).
Задача 3. Вампиризм (Взаимодействие методов)
Добавьте новому классу монстров Vampire способность лечиться при атаке.
Логика:
Каждый раз, когда Вампир наносит урон врагу, он восстанавливает себе здоровье в размере 10% от нанесенного урона.
Подсказка: Вам нужно переопределить метод attack. Сначала нанесите урон (через super().attack() или напрямую), а затем увеличьте self.health.
Задача 4. Система уровней (Level Up)
Это задача на изменение базового класса Character или Hero.
1. Добавьте атрибуты self.xp = 0 (опыт) и self.level = 1.
2. Напишите метод gain_xp(self, amount).
3. Если опыт переваливает за 100, уровень повышается: - self.level += 1 - self.xp сбрасывается (или вычитается 100). - Урон и здоровье героя увеличиваются на 10%. - В консоль выводится сообщение: "? УРОВЕНЬ ПОВЫШЕН! Теперь уровень 2".
Задача 5. Хардкор: Битва стенка на стенку (Списки и Циклы)
Измените Game Loop (основной цикл игры) так, чтобы дрались не 1 на 1, а команда на команду.
1. Создайте список heroes = [Warrior(...), Mage(...)].
2. Создайте список enemies = [Orc(...), Orc(...), Orc(...)].
3. В цикле случайно выбирайте атакующего из одной команды и жертву из другой.
4. Если здоровье персонажа падает до 0, удаляйте его из списка (метод list.remove() или создание нового списка).
5. Игра заканчивается, когда один из списков пуст.
Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.
Уверен, у вас все получится. Вперед, к экспериментам
Комментарии (7)

tenzink
22.11.2025 11:55Какое-то месиво получается. Класс персонажа и в консоль пишет, и random зовет. Страшно представить в какого монстра все это превратится, когда начнут появляться новые требования: damage зависит от типа местности, от того кто кого атакует, при атаке можно контратаковать, но не всех и т.п. Представьте боевую систему третьих героев, или wesnoth, и как вы будете реализовывать подобное

enamored_poc Автор
22.11.2025 11:55Вы приводите примеры сложных систем (Heroes, Wesnoth), а мы здесь разбираем алфавит. Конечно, архитектура AAA-стратегии строится иначе (ECS, Event Bus и т.д.). Но нельзя научить человека писать «Войну и мир», пока он не выучил буквы. Этот код — демонстрация базовых принципов (Наследование/Полиморфизм) в вакууме, а не готовый движок для Steam.

enamored_poc Автор
22.11.2025 11:55Звучит как суровое, но справедливое код-ревью времен моей стажировки в команде АБТ :) (zaplavs)
Вы абсолютно правы насчет архитектуры серьезных проектов (разделение логики, View и т.д.). Но если я сейчас вывалю на новичка паттерны проектирования, он просто закроет вкладку. Здесь мы намеренно упрощаем (KISS), чтобы человек понял суть self и полиморфизма. А до "серьезной архитектуры" мы с читателями дорастем в следующих статьях!
VAF34
Очень четко и понятно!
enamored_poc Автор
Пожалуйста)