Итоговый проект: RPG-игра на Python
Вы прошли весь курс ООП — классы, наследование, полиморфизм, магические методы, инкапсуляция, абстракция, паттерны. Теперь применим всё это в одном настоящем проекте. Мы построим небольшую 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(). Прогресс не теряется.
🌟 Следующие шаги: попробуйте реализовать одно из самостоятельных заданий. Начните с сохранения инвентаря или нового класса врага. Потом — система комнат. Каждое добавление закрепляет то, что вы изучили в курсе. Удачи, разработчик!