$ sudo teach IT
МОДУЛЬ 12 · УРОК 1

Итоговый проект: RPG-игра на Python

Вы прошли весь курс ООП — классы, наследование, полиморфизм, магические методы, инкапсуляция, абстракция, паттерны. Теперь применим всё это в одном настоящем проекте. Мы построим небольшую RPG-игру: персонажи, враги с разными атаками, инвентарь, бои, опыт, уровни и сохранение игры. Каждая концепция курса найдёт своё место.

⏱ ~90 минут 🎓 Итоговый проект 🐍 Python 3 ⚔️ RPG 🏆 Финал курса

🗺️ Обзор: что мы построим

RPG-игра — идеальный проект для итога курса ООП. Здесь всё естественно ложится на классы: персонажи — объекты, бои — методы, предметы — отдельная иерархия. Посмотрим на финальную картину целиком, прежде чем погружаться в код.

⚔️

Персонажи и враги

Иерархия: Character → Hero / Enemy → Goblin, Orc, Dragon. Полиморфизм: каждый враг атакует по-своему.

🎒

Инвентарь и предметы

Класс Inventory внутри Hero (композиция). Иерархия предметов: Item → Weapon, Armor, Potion.

📈

Бои и прогрессия

Механика урона: attack - defense. Опыт за победу, прокачка уровня, рост характеристик.

💾

Сохранение через JSON

Сохраняем состояние героя в JSON-файл, загружаем обратно. Практика сериализации.

📦

Пакетная структура

Код разбит на модули в пакете rpg/. Файл main.py — точка входа.

✨

Магические методы

__str__ для красивого вывода персонажей, __lt__ для сортировки по уровню.

💡 Какие концепции курса используются: наследование, полиморфизм, инкапсуляция, абстрактные классы, магические методы, композиция, исключения, JSON-сериализация. Это не учебный «Hello World» — это полноценный мини-проект, который вы сможете расширять дальше самостоятельно.

📁 Структура проекта

Код разбиваем на логические модули. Каждый файл отвечает за свою часть. Вот итоговая структура — создайте её прямо сейчас, прежде чем мы начнём писать код:

rpg_game/                ← корневая папка проекта
│
├── rpg/                 ← пакет (папка с __init__.py)
│   ├── __init__.py      ← делает папку пакетом
│   ├── characters.py    ← Character, Hero, Enemy
│   ├── enemies.py       ← Goblin, Orc, Dragon
│   ├── items.py         ← Item, Weapon, Armor, Potion
│   └── inventory.py     ← Inventory
│
└── main.py              ← точка входа, демонстрация игры

📌 Как создать структуру

Создайте папку rpg_game, внутри — папку rpg с пустым файлом __init__.py. Остальные файлы будем заполнять по шагам. Файл main.py — в корне проекта, рядом с папкой rpg.

🧬 Проектирование иерархии персонажей

Прежде чем писать код — нарисуем структуру наследования. Это важный навык: сначала думаем, потом кодируем.

Дерево наследования:

Character (базовый, абстрактный)
    ├── Hero       ← управляется игроком, имеет инвентарь
    └── Enemy      ← базовый враг (абстрактный)
            ├── Goblin   ← атакует ядом
            ├── Orc      ← сильный удар, может оглушить
            └── Dragon   ← огненное дыхание (урон по площади)

Атрибуты базового персонажа — всё, что есть и у героя, и у врагов:

name

Имя персонажа

hp / max_hp

Текущее и максимальное HP

attack

Сила атаки

defense

Защита

level

Уровень персонажа

exp

Накопленный опыт

🗡️ Шаг 1: rpg/items.py — предметы

Начнём с предметов — они не зависят от других модулей, поэтому их проще написать первыми. Три типа предметов: оружие (повышает атаку), броня (повышает защиту), зелье (восстанавливает HP).

# rpg/items.py
from abc import ABC, abstractmethod


class Item(ABC):
    """Базовый абстрактный класс для всех предметов."""

    def __init__(self, name: str, description: str = "") -> None:
        self.name = name
        self.description = description

    @abstractmethod
    def use(self, character) -> str:
        """Применить предмет к персонажу. Возвращает строку-описание эффекта."""
        pass

    def __str__(self) -> str:
        return f"[{self.__class__.__name__}] {self.name}"

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}('{self.name}')"


class Weapon(Item):
    """
    Оружие: повышает атаку персонажа.
    bonus_attack — прибавка к атаке при экипировке.
    """

    def __init__(self, name: str, bonus_attack: int, description: str = "") -> None:
        super().__init__(name, description)
        self.bonus_attack = bonus_attack

    def use(self, character) -> str:
        character.attack += self.bonus_attack
        return f"{character.name} экипировал {self.name} (+{self.bonus_attack} к атаке)"


class Armor(Item):
    """
    Броня: повышает защиту персонажа.
    bonus_defense — прибавка к защите при экипировке.
    """

    def __init__(self, name: str, bonus_defense: int, description: str = "") -> None:
        super().__init__(name, description)
        self.bonus_defense = bonus_defense

    def use(self, character) -> str:
        character.defense += self.bonus_defense
        return f"{character.name} надел {self.name} (+{self.bonus_defense} к защите)"


class Potion(Item):
    """
    Зелье: восстанавливает HP.
    heal_amount — сколько HP восстанавливается.
    """

    def __init__(self, name: str, heal_amount: int, description: str = "") -> None:
        super().__init__(name, description)
        self.heal_amount = heal_amount

    def use(self, character) -> str:
        old_hp = character.hp
        # HP не может превысить максимум
        character.hp = min(character.hp + self.heal_amount, character.max_hp)
        actual_heal = character.hp - old_hp
        return f"{character.name} выпил {self.name} (+{actual_heal} HP, итого: {character.hp}/{character.max_hp})"

🔍 Разбор: почему именно так

  • Метод use(character) принимает персонажа и изменяет его атрибуты — это полиморфизм: один вызов, разные эффекты.
  • super().__init__(name, description) — вызываем конструктор родителя, не дублируем код.
  • В Potion.use() ограничиваем HP через min(..., character.max_hp) — зелье не может «перелечить» выше максимума.
  • Item — абстрактный: нельзя создать просто «предмет без типа», только конкретный вид.

🎒 Шаг 2: rpg/inventory.py — инвентарь

Inventory — это отдельный класс, который будет встроен в Hero как атрибут. Это паттерн композиция: Hero «имеет» инвентарь, а не «является» инвентарём. Инвентарь умеет добавлять предметы, показывать их список и использовать зелья.

# rpg/inventory.py
from typing import List, Optional
from rpg.items import Item, Potion


class Inventory:
    """
    Инвентарь героя (композиция внутри Hero).
    Хранит список предметов, умеет добавлять и использовать их.
    """

    def __init__(self, max_size: int = 10) -> None:
        self._items: List[Item] = []
        self.max_size = max_size

    def add_item(self, item: Item) -> bool:
        """
        Добавить предмет. Возвращает True если успешно, False если инвентарь полон.
        """
        if len(self._items) >= self.max_size:
            print(f"  ⚠️ Инвентарь полон! Максимум {self.max_size} предметов.")
            return False
        self._items.append(item)
        print(f"  🎒 Получен предмет: {item.name}")
        return True

    def remove_item(self, item: Item) -> bool:
        """Убрать предмет из инвентаря."""
        if item in self._items:
            self._items.remove(item)
            return True
        return False

    def use_potion(self, character) -> Optional[str]:
        """
        Найти первое зелье в инвентаре, использовать на персонаже и убрать.
        Возвращает строку-описание или None если зелий нет.
        """
        for item in self._items:
            if isinstance(item, Potion):
                result = item.use(character)
                self._items.remove(item)   # зелье одноразовое
                return result
        return None

    def show(self) -> None:
        """Показать содержимое инвентаря."""
        if not self._items:
            print("  Инвентарь пуст.")
            return
        print(f"  Инвентарь ({len(self._items)}/{self.max_size}):")
        for i, item in enumerate(self._items, 1):
            print(f"    {i}. {item}")

    def __len__(self) -> int:
        return len(self._items)

    def __contains__(self, item: Item) -> bool:
        return item in self._items

💡 Зачем выносить инвентарь в отдельный класс?

Если бы мы хранили список предметов прямо в Hero, логика управления предметами смешалась бы с логикой боя. Отдельный класс — это разделение ответственности (SRP): каждый класс делает одно дело. Inventory управляет предметами, Hero управляет персонажем. Позже инвентарь можно усложнять независимо от героя.

🧙 Шаг 3: rpg/characters.py — персонажи

Центральный модуль проекта. Базовый класс Character, затем Hero с прогрессией и инвентарём, и Enemy как абстрактный родитель для всех врагов.

# rpg/characters.py
from abc import ABC, abstractmethod
from rpg.inventory import Inventory


class Character(ABC):
    """
    Базовый абстрактный класс для всех персонажей.
    Содержит общую логику: атрибуты, проверку здоровья, получение урона.
    """

    def __init__(
        self,
        name: str,
        hp: int,
        attack: int,
        defense: int,
        level: int = 1
    ) -> None:
        self.name = name
        self.max_hp = hp
        self.hp = hp
        self.attack = attack
        self.defense = defense
        self.level = level
        self.exp = 0

    @property
    def is_alive(self) -> bool:
        """Жив ли персонаж."""
        return self.hp > 0

    def take_damage(self, damage: int) -> int:
        """
        Получить урон. Возвращает фактически полученный урон.
        Формула: урон = damage - defense, минимум 1.
        """
        actual_damage = max(1, damage - self.defense)
        self.hp = max(0, self.hp - actual_damage)
        return actual_damage

    @abstractmethod
    def do_attack(self, target: "Character") -> str:
        """Атаковать цель. Возвращает описание атаки."""
        pass

    def __str__(self) -> str:
        return (
            f"{self.name} [Lv.{self.level}] "
            f"HP: {self.hp}/{self.max_hp} "
            f"ATK: {self.attack} DEF: {self.defense}"
        )

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}('{self.name}', level={self.level})"

    def __lt__(self, other: "Character") -> bool:
        """Сортировка персонажей по уровню (для sorted(), min(), max())."""
        return self.level < other.level


# ══════════════════════════════════════════════
# ГЕРОЙ
# ══════════════════════════════════════════════

class Hero(Character):
    """
    Герой — управляемый игроком персонаж.
    Дополнительно: инвентарь (композиция), система прокачки уровней.
    """

    # Опыт, нужный для каждого уровня (индекс = уровень)
    EXP_TO_LEVEL = [0, 0, 100, 250, 450, 700, 1000, 1400, 1900, 2500, 3200]

    def __init__(self, name: str, hp: int = 100, attack: int = 15, defense: int = 5) -> None:
        super().__init__(name, hp, attack, defense)
        self.inventory = Inventory()   # композиция: Hero «имеет» Inventory

    def do_attack(self, target: "Character") -> str:
        """Базовая атака героя."""
        damage = target.take_damage(self.attack)
        return f"  ⚔️ {self.name} атакует {target.name} — {damage} урона (HP: {target.hp}/{target.max_hp})"

    def gain_exp(self, amount: int) -> None:
        """Начислить опыт. При достижении нужного порога — повысить уровень."""
        self.exp += amount
        print(f"  ✨ {self.name} получает {amount} опыта (всего: {self.exp})")
        self._check_level_up()

    def _check_level_up(self) -> None:
        """Проверить и применить повышение уровня."""
        max_level = len(self.EXP_TO_LEVEL) - 1
        while self.level < max_level and self.exp >= self.EXP_TO_LEVEL[self.level + 1]:
            self.level += 1
            # Рост характеристик при прокачке
            self.max_hp += 20
            self.hp = self.max_hp   # полное восстановление HP
            self.attack += 3
            self.defense += 2
            print(f"\n  🎉 LEVEL UP! {self.name} достиг уровня {self.level}!")
            print(f"     HP: {self.max_hp}  ATK: {self.attack}  DEF: {self.defense}")

    def use_potion(self) -> None:
        """Использовать зелье из инвентаря."""
        result = self.inventory.use_potion(self)
        if result:
            print(f"  🧪 {result}")
        else:
            print(f"  ❌ {self.name}: зелий в инвентаре нет!")


# ══════════════════════════════════════════════
# БАЗОВЫЙ ВРАГ
# ══════════════════════════════════════════════

class Enemy(Character):
    """
    Базовый класс врага.
    exp_reward — опыт, который враг даёт при победе над ним.
    Конкретные враги переопределяют do_attack() — полиморфизм.
    """

    def __init__(
        self,
        name: str,
        hp: int,
        attack: int,
        defense: int,
        level: int = 1,
        exp_reward: int = 50
    ) -> None:
        super().__init__(name, hp, attack, defense, level)
        self.exp_reward = exp_reward

    def do_attack(self, target: "Character") -> str:
        """Базовая атака врага (переопределяется в подклассах)."""
        damage = target.take_damage(self.attack)
        return f"  👊 {self.name} ударяет {target.name} — {damage} урона (HP: {target.hp}/{target.max_hp})"

🔍 Ключевые решения

  • is_alive — @property, а не метод: читается как атрибут (hero.is_alive), но вычисляется на лету.
  • take_damage() — в базовом классе. Логика одна для всех: не нужно дублировать в Hero и Enemy.
  • __lt__ позволяет сортировать персонажей: sorted([goblin, orc, dragon]) — по уровню.
  • EXP_TO_LEVEL — таблица опыта как константа класса. Легко изменить баланс игры.
  • _check_level_up() — приватный метод (подчёркивание). Внешний код не должен вызывать его напрямую.

👹 Шаг 4: rpg/enemies.py — враги с уникальными атаками

Три типа врагов — и каждый атакует по-своему. Это классический полиморфизм: один метод do_attack(), три разных реализации. Код боя не будет знать, с каким врагом работает — он просто вызывает enemy.do_attack(hero).

# rpg/enemies.py
import random
from rpg.characters import Enemy, Character


class Goblin(Enemy):
    """
    Гоблин — слабый враг, но атакует ядом.
    Яд наносит дополнительный урон поверх обычного.
    poison_damage — урон от яда (сверх обычной защиты).
    """

    def __init__(self, name: str = "Гоблин", level: int = 1) -> None:
        super().__init__(
            name=name,
            hp=40 + level * 5,
            attack=8 + level * 2,
            defense=2 + level,
            level=level,
            exp_reward=30 + level * 10
        )
        self.poison_damage = 3 + level  # яд пробивает броню

    def do_attack(self, target: "Character") -> str:
        # Обычный урон
        normal_damage = target.take_damage(self.attack)
        # Ядовитый урон — игнорирует броню (наносим напрямую)
        target.hp = max(0, target.hp - self.poison_damage)
        return (
            f"  🟢 {self.name} атакует ядовитым укусом! "
            f"{normal_damage} урона + {self.poison_damage} яда "
            f"(HP: {target.hp}/{target.max_hp})"
        )


class Orc(Enemy):
    """
    Орк — сильный враг. Раз в несколько ударов бьёт с двойной силой.
    charge_chance — вероятность удара с разбега.
    """

    def __init__(self, name: str = "Орк", level: int = 2) -> None:
        super().__init__(
            name=name,
            hp=80 + level * 10,
            attack=12 + level * 3,
            defense=6 + level * 2,
            level=level,
            exp_reward=60 + level * 15
        )
        self.charge_chance = 0.30  # 30% вероятность усиленного удара

    def do_attack(self, target: "Character") -> str:
        # Проверяем: будет ли удар с разбега?
        if random.random() < self.charge_chance:
            # Усиленный удар: двойная атака
            damage = target.take_damage(self.attack * 2)
            return (
                f"  🔴 {self.name} бьёт с РАЗБЕГА! "
                f"{damage} урона "
                f"(HP: {target.hp}/{target.max_hp})"
            )
        else:
            damage = target.take_damage(self.attack)
            return (
                f"  🟠 {self.name} наносит удар — "
                f"{damage} урона "
                f"(HP: {target.hp}/{target.max_hp})"
            )


class Dragon(Enemy):
    """
    Дракон — босс. Атакует огнём, поражая цель с уроном по площади.
    breath_damage — фиксированный урон огнём (игнорирует броню частично).
    """

    def __init__(self, name: str = "Дракон", level: int = 5) -> None:
        super().__init__(
            name=name,
            hp=200 + level * 20,
            attack=20 + level * 5,
            defense=10 + level * 3,
            level=level,
            exp_reward=200 + level * 50
        )
        self.breath_damage = 15 + level * 3  # огненный урон игнорирует половину брони

    def do_attack(self, target: "Character") -> str:
        # Огненное дыхание: игнорирует половину брони цели
        effective_defense = target.defense // 2
        actual_damage = max(1, self.breath_damage - effective_defense)
        target.hp = max(0, target.hp - actual_damage)
        return (
            f"  🔥 {self.name} выдыхает ПЛАМЯ! "
            f"{actual_damage} огненного урона (половина брони не помогает) "
            f"(HP: {target.hp}/{target.max_hp})"
        )

🟢 Goblin — яд

Два источника урона: обычный (с учётом защиты) + яд (игнорирует броню). Опасен на низком уровне героя.

🟠 Orc — разбег

30% шанс удвоить урон. Используем random.random() для элемента случайности — как в настоящих RPG.

🔥 Dragon — пламя

Фиксированный огненный урон, игнорирует половину брони. Босс: высокие статы, много опыта.

📦 Шаг 5: rpg/__init__.py — экспорты пакета

Файл __init__.py делает папку rpg пакетом и удобно экспортирует все классы. Благодаря этому в main.py мы напишем короткий импорт вместо длинного.

# rpg/__init__.py

from rpg.items import Item, Weapon, Armor, Potion
from rpg.inventory import Inventory
from rpg.characters import Character, Hero, Enemy
from rpg.enemies import Goblin, Orc, Dragon

__all__ = [
    "Item", "Weapon", "Armor", "Potion",
    "Inventory",
    "Character", "Hero", "Enemy",
    "Goblin", "Orc", "Dragon",
]

💡 Зачем __all__? Это список, который определяет, что будет экспортироваться при from rpg import *. Также служит документацией: читая __init__.py, сразу видно, что «публичного» в пакете. Внутренние детали — в самих модулях.

Благодаря __init__.py, импорт в main.py выглядит чисто:

# В main.py — чистый импорт из пакета
from rpg import Hero, Goblin, Orc, Dragon, Weapon, Armor, Potion

# БЕЗ __init__.py пришлось бы писать:
# from rpg.characters import Hero
# from rpg.enemies import Goblin, Orc, Dragon
# from rpg.items import Weapon, Armor, Potion
# ... и так далее

⚔️ Шаг 6: Механика боя

Теперь напишем функцию боя — она принимает двух персонажей и проводит поединок до гибели одного из них. Это не метод класса — это отдельная игровая функция. Она не привязана ни к герою, ни к врагу, а работает с любой парой персонажей.

# В main.py (или в отдельном rpg/battle.py)
from rpg import Hero, Enemy


def battle(hero: Hero, enemy: Enemy) -> bool:
    """
    Провести бой между героем и врагом.
    Возвращает True если герой победил, False если проиграл.
    Бой идёт по очерёдности: сначала атакует герой, потом враг.
    """
    print(f"\n{'='*50}")
    print(f"  ⚔️  БОЙ: {hero.name} vs {enemy.name}")
    print(f"  {'='*46}")
    print(f"  {hero}")
    print(f"  {enemy}")
    print(f"{'='*50}")

    round_num = 1

    while hero.is_alive and enemy.is_alive:
        print(f"\n--- Раунд {round_num} ---")

        # Ход героя
        result = hero.do_attack(enemy)
        print(result)

        # Проверяем: не умер ли враг после атаки героя
        if not enemy.is_alive:
            break

        # Ход врага
        result = enemy.do_attack(hero)
        print(result)

        round_num += 1

    print(f"\n{'='*50}")

    if hero.is_alive:
        print(f"  🏆 Победа! {hero.name} победил {enemy.name}!")
        hero.gain_exp(enemy.exp_reward)
        return True
    else:
        print(f"  💀 {hero.name} погиб в бою с {enemy.name}...")
        return False

🔍 Почему функция, а не метод?

Бой — это взаимодействие двух объектов. Сделать его методом hero.fight(enemy) — значит дать герою знание о враге. Сделать методом enemy.fight(hero) — симметричная проблема. Отдельная функция battle(hero, enemy) — нейтральная позиция. Ни герой, ни враг не «управляют» боем; оба просто участвуют.

💾 Шаг 7: Сохранение и загрузка через JSON

Добавим возможность сохранять прогресс героя в файл и загружать его обратно. Используем стандартную библиотеку json — никаких дополнительных установок. Это простейший способ сериализации в Python.

Добавим два метода в класс Hero:

# Добавляем в rpg/characters.py, в класс Hero:
import json


class Hero(Character):
    # ... (существующий код) ...

    def to_dict(self) -> dict:
        """
        Сериализовать героя в словарь.
        Только ключевые атрибуты прогресса — без инвентаря (упрощение).
        """
        return {
            "name": self.name,
            "level": self.level,
            "exp": self.exp,
            "hp": self.hp,
            "max_hp": self.max_hp,
            "attack": self.attack,
            "defense": self.defense,
        }

    def save(self, filepath: str = "save.json") -> None:
        """Сохранить состояние героя в JSON-файл."""
        data = self.to_dict()
        with open(filepath, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"  💾 Игра сохранена: {filepath}")

    @classmethod
    def load(cls, filepath: str = "save.json") -> "Hero":
        """
        Загрузить героя из JSON-файла.
        Классовый метод: создаёт и возвращает новый объект Hero.
        """
        with open(filepath, "r", encoding="utf-8") as f:
            data = json.load(f)

        hero = cls(
            name=data["name"],
            hp=data["max_hp"],
            attack=data["attack"],
            defense=data["defense"],
        )
        # Восстанавливаем прогресс
        hero.level = data["level"]
        hero.exp = data["exp"]
        hero.hp = data["hp"]

        print(f"  📂 Игра загружена: {hero.name} (Lv.{hero.level})")
        return hero

🔍 Разбор: to_dict() и @classmethod

  • to_dict() — превращает объект в обычный Python-словарь. JSON умеет работать только с базовыми типами: строки, числа, списки, словари.
  • json.dump(..., indent=2) — записывает с отступами, файл читается человеком.
  • @classmethod def load(cls, ...) — создаёт новый объект, не изменяет существующий. Вызов: hero = Hero.load("save.json").
  • Инвентарь не сохраняется — это упрощение. В самостоятельном задании предлагается это исправить.

Как выглядит файл save.json:

{
  "name": "Артур",
  "level": 3,
  "exp": 320,
  "hp": 140,
  "max_hp": 140,
  "attack": 21,
  "defense": 9
}

🎮 Шаг 8: main.py — полная демонстрация игры

Собираем всё вместе. Главный файл проекта — точка входа. Здесь создаём героя, снаряжаем его, проводим несколько боёв, сохраняем прогресс и загружаем.

# main.py
import json
from rpg import Hero, Goblin, Orc, Dragon, Weapon, Armor, Potion


def battle(hero: Hero, enemy) -> bool:
    """Провести бой. True — победа героя, False — поражение."""
    print(f"\n{'='*50}")
    print(f"  ⚔️  БОЙ: {hero.name} vs {enemy.name}")
    print(f"  {'-'*46}")
    print(f"  {hero}")
    print(f"  {enemy}")
    print(f"{'='*50}")

    round_num = 1
    while hero.is_alive and enemy.is_alive:
        print(f"\n--- Раунд {round_num} ---")
        print(hero.do_attack(enemy))
        if not enemy.is_alive:
            break
        print(enemy.do_attack(hero))
        round_num += 1

    print(f"\n{'='*50}")
    if hero.is_alive:
        print(f"  🏆 {hero.name} победил {enemy.name}!")
        hero.gain_exp(enemy.exp_reward)
        return True
    else:
        print(f"  💀 {hero.name} погиб...")
        return False


def main():
    print("╔══════════════════════════════════╗")
    print("║     ⚔️   PYTHON RPG GAME   ⚔️     ║")
    print("╚══════════════════════════════════╝\n")

    # ── Создаём героя ─────────────────────────
    hero = Hero(name="Артур", hp=100, attack=15, defense=5)
    print(f"Герой создан: {hero}\n")

    # ── Снаряжаем героя ───────────────────────
    sword = Weapon("Короткий меч", bonus_attack=5, description="Простой стальной меч")
    leather_armor = Armor("Кожаная броня", bonus_defense=3)
    health_potion = Potion("Зелье лечения", heal_amount=40)
    big_potion = Potion("Большое зелье", heal_amount=70)

    print("Снаряжаем героя:")
    print(f"  {sword.use(hero)}")
    print(f"  {leather_armor.use(hero)}")
    hero.inventory.add_item(health_potion)
    hero.inventory.add_item(big_potion)

    print(f"\nПосле снаряжения: {hero}")
    print("\nИнвентарь:")
    hero.inventory.show()

    # ── Бой 1: Гоблин ─────────────────────────
    goblin = Goblin(name="Гоблин Зуб", level=1)
    if not battle(hero, goblin):
        print("\nКонец игры.")
        return

    # Используем зелье если HP ниже 60%
    if hero.hp < hero.max_hp * 0.6:
        print(f"\n  🩺 HP низкое ({hero.hp}/{hero.max_hp}), используем зелье...")
        hero.use_potion()

    # ── Бой 2: Орк ────────────────────────────
    orc = Orc(name="Орк Громила", level=2)
    if not battle(hero, orc):
        print("\nКонец игры.")
        return

    # ── Сохраняем прогресс ────────────────────
    print("\n" + "─" * 50)
    hero.save("save.json")

    # ── Симуляция загрузки ────────────────────
    print("\n[Симулируем перезапуск игры...]")
    loaded_hero = Hero.load("save.json")
    print(f"Загруженный герой: {loaded_hero}")

    # ── Бой 3: Дракон (с загруженным героем) ─
    # Добавим ещё зелье загруженному герою
    loaded_hero.inventory.add_item(Potion("Эликсир силы", heal_amount=100))

    dragon = Dragon(name="Дракон Игнис", level=4)
    result = battle(loaded_hero, dragon)

    # ── Итог ─────────────────────────────────
    print(f"\n{'═'*50}")
    if result:
        print(f"  🎊 Все враги побеждены! {loaded_hero.name} — легенда!")
    else:
        print(f"  📜 Дракон непобедим... но приключение продолжается.")
    print(f"  Итог: {loaded_hero}")

    # ── Демонстрация сортировки ───────────────
    print(f"\n{'─'*50}")
    print("Сортировка врагов по уровню (от слабого к сильному):")
    all_enemies = [
        Goblin("Гоблин А", level=1),
        Dragon("Дракон Б", level=5),
        Orc("Орк В", level=2),
        Goblin("Гоблин Г", level=3),
    ]
    sorted_enemies = sorted(all_enemies)  # используем __lt__
    for e in sorted_enemies:
        print(f"  Lv.{e.level} — {e.name} ({e.__class__.__name__})")


if __name__ == "__main__":
    main()

Пример вывода (первые бои):

╔══════════════════════════════════╗
║     ⚔️   PYTHON RPG GAME   ⚔️     ║
╚══════════════════════════════════╝

Герой создан: Артур [Lv.1] HP: 100/100 ATK: 15 DEF: 5

Снаряжаем героя:
  Артур экипировал Короткий меч (+5 к атаке)
  Артур надел Кожаная броня (+3 к защите)
  🎒 Получен предмет: Зелье лечения
  🎒 Получен предмет: Большое зелье

После снаряжения: Артур [Lv.1] HP: 100/100 ATK: 20 DEF: 8

==================================================
  ⚔️  БОЙ: Артур vs Гоблин Зуб
  --------------------------------------------------
  Артур [Lv.1] HP: 100/100 ATK: 20 DEF: 8
  Гоблин Зуб [Lv.1] HP: 45/45 ATK: 10 DEF: 3
==================================================

--- Раунд 1 ---
  ⚔️ Артур атакует Гоблин Зуб — 17 урона (HP: 28/45)
  🟢 Гоблин Зуб атакует ядовитым укусом! 2 урона + 4 яда (HP: 94/100)

--- Раунд 2 ---
  ⚔️ Артур атакует Гоблин Зуб — 17 урона (HP: 11/45)
  🟢 Гоблин Зуб атакует ядовитым укусом! 2 урона + 4 яда (HP: 88/100)

--- Раунд 3 ---
  ⚔️ Артур атакует Гоблин Зуб — 17 урона (HP: 0/45)

==================================================
  🏆 Артур победил Гоблин Зуб!
  ✨ Артур получает 40 опыта (всего: 40)

🔗 Карта концепций в проекте

Посмотрим, какие концепции курса мы использовали — и где именно они появляются:

🧬 Наследование

Character → Hero, Character → Enemy → Goblin/Orc/Dragon, Item → Weapon/Armor/Potion. Общий код в базовых классах, детали — в подклассах.

🔄 Полиморфизм

do_attack() у каждого врага своя. Метод use() у предметов. Функция battle() не знает с каким именно врагом работает.

🔒 Инкапсуляция

_items в инвентаре. _check_level_up() в Hero. _notify_all() — приватные методы скрыты от внешнего кода.

🧩 Абстракция

Character и Item — абстрактные классы с @abstractmethod. Нельзя создать «просто персонажа» без конкретного типа.

🔗 Композиция

Hero «имеет» Inventory. Не наследует, а содержит. Инвентарь можно заменить или расширить независимо.

✨ Магические методы

__str__ для красивого вывода, __repr__ для отладки, __lt__ для сортировки, __len__/__contains__ в Inventory.

📝 Свойства

@property is_alive — читается как атрибут, но вычисляется. Чистый интерфейс без лишних скобок.

💾 Сериализация

to_dict(), save(), @classmethod load(). JSON для сохранения прогресса. Классовый метод для восстановления объекта.

⚠️ Типичные ошибки при работе с проектом

❌ Проблема: ModuleNotFoundError

Ошибка при импорте: ModuleNotFoundError: No module named 'rpg'

✅ Решение: запускайте main.py из корневой папки rpg_game/, а не из папки rpg/. Команда: python main.py (находясь в rpg_game/).

❌ Проблема: циклические импорты

Если characters.py импортирует из enemies.py, а enemies.py — из characters.py — будет циклический импорт.

✅ Решение: строго соблюдайте иерархию. items.py → inventory.py → characters.py → enemies.py. Каждый файл импортирует только из предыдущих.

❌ Проблема: HP уходит в минус

Если не ограничить HP снизу, персонаж «умрёт» с отрицательными HP, что ломает логику is_alive.

✅ Решение: всегда используйте max(0, self.hp - damage). Это уже есть в методе take_damage() базового класса.

🚀 Самостоятельные задания для расширения

Проект намеренно оставлен с пространством для роста. Вот список улучшений — от простых к сложным. Попробуйте реализовать хотя бы одно самостоятельно:

Уровень: легко

💊 Сохранение инвентаря

Добавьте сохранение предметов инвентаря в JSON. Для этого расширьте to_dict() в Hero и load() для восстановления предметов.

Уровень: легко

🧙 Новый враг: Волшебник

Создайте класс Wizard(Enemy): атака заклинанием, которое игнорирует всю защиту. Добавьте его в бой.

Уровень: средне

🗺️ Система комнат

Создайте класс Room со списком врагов и предметов. После победы над всеми врагами — переход в следующую комнату.

Уровень: средне

🛡️ Активные навыки

Добавьте классу Hero список навыков (Skill) с cooldown. Например: «Мощный удар» (2× урон, cooldown 3 раунда).

Уровень: сложно

🤝 Бой 2 на 2

Расширьте функцию battle(): партия из двух героев против двух врагов. Каждый ход — атакует каждый персонаж.

Уровень: сложно

📊 Лог боя в файл

Подключите стандартный модуль logging. Записывайте каждый раунд в файл battle.log. Реализуйте «режим replay»: воспроизводите записанный бой.

🎓 Поздравляем — вы завершили курс ООП на Python!

В этом проекте вы применили все ключевые концепции объектно-ориентированного программирования в одной связной системе. Не просто изучили теорию — написали настоящую игру с нуля.

🧬 Иерархия классов

Четыре уровня наследования. Общий код — в базовых классах. Уникальное — в подклассах.

🔄 Полиморфизм в действии

Гоблин, Орк, Дракон — разные атаки через один интерфейс. Код боя не зависит от типа врага.

🔗 Композиция

Инвентарь — отдельный класс внутри Героя. Независим, расширяем, инкапсулирован.

✨ Магические методы

__str__, __lt__, __len__, __contains__ — объекты ведут себя как встроенные типы.

📦 Пакетная структура

Код организован в модули. Каждый файл — своя ответственность. Импорты через __init__.py.

💾 Сохранение данных

JSON-сериализация через to_dict() и классовый метод load(). Прогресс не теряется.

🌟 Следующие шаги: попробуйте реализовать одно из самостоятельных заданий. Начните с сохранения инвентаря или нового класса врага. Потом — система комнат. Каждое добавление закрепляет то, что вы изучили в курсе. Удачи, разработчик!