RPG игра

Вася решил заняться разработкой компьютерных игр (кстати python применяется даже в геймдеве!). Ему поручили разрабатывать искусственный интеллект для союзников, которые сражаются бок о бок с реальными игроками. Но так как Вася пока не силён в теме машинного обучения и нейросетей — ему предстоит заменить эти знания смекалкой и набором if/else условий.

Вася уже написал код, описывающий монстров (файл monsters.py), этот код изменять нельзя. В файле heroes.py вы найдете заготовки системы классов: — базовый класс Hero, который нельзя изменять — наследники класса Tank/Healer/Attacker — их надо изменять

Помимо этого в main.py есть код который:

  1. запускает 1 год сражений — изменять нельзя
  2. создает команду для сражения с монстрами — изменять можно, но с условиями
  3. запускает 20 раз 1 год сражений и подсчитывает количество побед — изменять нельзя

Ваша задача:

  1. Дописать код в классы Tank/Healer/Attacker в файле heroes.py
  2. Сформировать команду в main.py
  3. Проверить, что с выбранной вами стратегией герои побеждают монстров как минимум в половине случаев (>=10 побед из 20)

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

Советы и рекомендации:

  • Внимательно изучите код поведения монстров (изменять его нельзя, но изучать не запрещено)
  • При помощи команды принт выводите информацию о том кто и что делает в каждый день (особое внимание уделите информации, которая идёт в последние дни перед поражением героев)
  • На основе полученной информации попробуйте изменять приоритеты действий (обратите внимание, что вы можете не только выбирать действие для выполнения, но также выбрать цель для действия. Иногда может быть выгоднее атаковать монстров конкретного класса, чтобы уменьшить урон по вашей команде.)
import random


class Hero:
    # Базовый класс, который не подлежит изменению
    # У каждого наследника будут атрибуты:
    # - Имя
    # - Здоровье
    # - Сила
    # - Жив ли объект
    # Каждый наследник будет уметь:
    # - Атаковать
    # - Получать урон
    # - Выбирать действие для выполнения
    # - Описывать своё состояние

    max_hp = 150
    start_power = 10

    def __init__(self, name):
        self.name = name
        self.__hp = self.max_hp
        self.__power = self.start_power
        self.__is_alive = True

    def get_hp(self):
        return self.__hp

    def set_hp(self, new_value):
        self.__hp = max(new_value, 0)

    def get_power(self):
        return self.__power

    def set_power(self, new_power):
        self.__power = new_power

    def is_alive(self):
        return self.__is_alive

    # Все наследники должны будут переопределять каждый метод базового класса (кроме геттеров/сеттеров)
    # Переопределенные методы должны вызывать методы базового класса (при помощи super).
    # Методы attack и __str__ базового класса можно не вызывать (т.к. в них нету кода).
    # Они нужны исключительно для наглядности.
    # Метод make_a_move базового класса могут вызывать только герои, не монстры.
    def attack(self, target):
        # Каждый наследник будет наносить урон согласно правилам своего класса
        raise NotImplementedError("Вы забыли переопределить метод Attack!")

    def take_damage(self, damage):
        # Каждый наследник будет получать урон согласно правилам своего класса
        # При этом у всех наследников есть общая логика, которая определяет жив ли объект.
        print("\t", self.name, "Получил удар с силой равной = ", round(damage), ". Осталось здоровья - ", round(self.get_hp()))
        # Дополнительные принты помогут вам внимательнее следить за боем и изменять стратегию, чтобы улучшить выживаемость героев
        if self.get_hp() <= 0:
            self.__is_alive = False

    def make_a_move(self, friends, enemies):
        # С каждым днём герои становятся всё сильнее.
        self.set_power(self.get_power() + 0.1)

    def __str__(self):
        # Каждый наследник должен выводить информацию о своём состоянии, чтобы вы могли отслеживать ход сражения
        raise NotImplementedError("Вы забыли переопределить метод __str__!")


class Healer(Hero):

    def __init__(self, name):
        super().__init__(name)
        self.magic_power = (self.get_power() * 3)

    def attack(self, target):
        target.take_damage(self.get_power() / 2)

    def take_damage(self, damage):
        self.set_hp(self.get_hp() - (1.2 * damage))
        super().take_damage(damage)

    def healing(self, target):
        target.set_hp((target.get_hp() + self.magic_power))

    def make_a_move(self, friends, enemies):
        for hero in friends:
            if hero.get_hp() < 150:
                self.healing(hero)
        else:
            target = random.choice(enemies)
            self.attack(target)
        super().make_a_move(friends, enemies)

    def __str__(self):
        return f'{self.name} имеет {self.get_hp()} здоровья и {self.get_power()} силы.'


# Целитель:
    # Атрибуты:
    # - магическая сила - равна значению НАЧАЛЬНОГО показателя силы умноженному на 3 (self.__power * 3)
    # Методы:
    # - атака - может атаковать врага, но атакует только в половину силы self.__power
    # - получение урона - т.к. защита целителя слаба - он получает на 20% больше урона (1.2 * damage)
    # - исцеление - увеличивает здоровье цели на величину равную своей магической силе
    # - выбор действия - получает на вход всех союзников и всех врагов и на основе своей стратегии выполняет ОДНО из действий (атака,
    # исцеление) на выбранную им цель


class Tank(Hero):
    def __init__(self, name):
        super().__init__(name)
        self.protection = 1
        self.shied_raised = True

    def attack(self, target):
        target.take_damage(self.get_power() / 2)

    def take_damage(self, damage):
        self.set_hp(self.get_hp() - (1.2 * damage))
        super().take_damage(damage)

    def raise_shield(self):
        if self.shied_raised == False:
            self.shied_raised = True
        if self.shied_raised == True:
            self.protection *= 2
            self.set_power(self.get_power() / 2)

    def lower_shield(self):
        if self.shied_raised == True:
            self.shied_raised = False
        if self.shied_raised == False:
            self.protection /= 2
            self.set_power(self.get_power() * 2)


    def make_a_move(self, friends, enemies):
        target = random.choice(enemies)
        self.attack(target)
        if self.shied_raised == False:
            self.raise_shield()
        super().make_a_move(friends, enemies)

    def __str__(self):
        return f'{self.name} имеет {self.get_hp()} здоровья и {self.get_power()} силы.'

    # Танк:
    # Атрибуты:
    # - показатель защиты - изначально равен 1, может увеличиваться и уменьшаться
    # - поднят ли щит - танк может поднимать щит, этот атрибут должен показывать поднят ли щит в данный момент
    # Методы:
    # - атака - атакует, но т.к. доспехи очень тяжелые - наносит половину урона (self.__power)
    # - получение урона - весь входящий урон делится на показатель защиты (damage/self.defense) и только потом отнимается от здоровья
    # - поднять щит - если щит не поднят - поднимает щит. Это увеличивает показатель брони в 2 раза, но уменьшает показатель силы в 2 раза.
    # - опустить щит - если щит поднят - опускает щит. Это уменьшает показатель брони в 2 раза, но увеличивает показатель силы в 2 раза.
    # - выбор действия - получает на вход всех союзников и всех врагов и на основе своей стратегии выполняет ОДНО из действий (атака,
    # поднять щит/опустить щит) на выбранную им цель


class Attacker(Hero):

    def __init__(self, name):
        super().__init__(name)
        self.damage_factor = 0

    def attack(self, target):
        target.take_damage(self.get_power() * self.damage_factor)
        self.power_dawn()

    def take_damage(self, damage):
        self.set_hp(self.get_hp() - (damage * (self.damage_factor / 2)))
        super().take_damage(damage)

    def power_dawn(self):
        self.damage_factor /= 2

    def power_up(self):
        self.damage_factor *= 2

    def make_a_move(self, friends, enemies):
        target = random.choice(enemies)
        if self.damage_factor < 5:
            self.power_up()
        else:
            self.attack(target)
        super().make_a_move(friends, enemies)

    def __str__(self):
        return f'{self.name} имеет {self.get_hp()} здоровья и {self.get_power()} силы.'

    # Убийца:
    # Атрибуты:
    # - коэффициент усиления урона (входящего и исходящего)
    # Методы:
    # - атака - наносит урон равный показателю силы (self.__power) умноженному на коэффициент усиления урона (self.power_multiply)
    # после нанесения урона - вызывается метод ослабления power_down.
    # - получение урона - получает урон равный входящему урона умноженному на половину коэффициента усиления урона - damage * (
    # self.power_multiply / 2)
    # - усиление (power_up) - увеличивает коэффициента усиления урона в 2 раза
    # - ослабление (power_down) - уменьшает коэффициента усиления урона в 2 раза
    # - выбор действия - получает на вход всех союзников и всех врагов и на основе своей стратегии выполняет ОДНО из действий (атака,
    # усиление, ослабление) на выбранную им цель
import random


class Monster:
    max_hp = 150
    start_power = 10

    def __init__(self, name):
        self.name = name
        self.__hp = self.max_hp
        self.__power = self.start_power
        self.__is_alive = True

    def get_hp(self):
        return self.__hp

    def set_hp(self, new_value):
        self.__hp = max(new_value, 0)

    def get_power(self):
        return self.__power

    def set_power(self, new_power):
        self.__power = new_power

    def attack(self, target):
        pass

    def is_alive(self):
        return self.__is_alive

    def take_damage(self, damage):
        print("\t", self.name, "Получил удар с силой равной = ", round(damage), ". Осталось здоровья - ", round(self.get_hp()))
        if self.get_hp() <= 0:
            self.__is_alive = False

    def make_a_move(self, friends, enemies):
        pass

    def __str__(self):
        return 'Name: {0} | HP: {1}'.format(self.name, self.get_hp())


class MonsterBerserk(Monster):

    def __init__(self, name):
        super().__init__(name)
        self.madness = 1

    def attack(self, target):
        target.take_damage(self.get_power() * self.madness)
        self.madness += 0.1

    def take_damage(self, power):
        self.set_hp(self.get_hp() - power * (self.madness / 2))
        if self.get_hp() < 50:
            self.madness *= 2
        super().take_damage(power)

    def make_a_move(self, friends, enemies):
        print(self.name, end=' ')
        self.madness = min(self.madness, 4)
        if not enemies:
            return
        if self.madness < 3:
            print("Атакую того, кто стоит ближе -", enemies[0].name)
            self.attack(enemies[0])
        else:
            target = random.choice(enemies)
            print("BERSERK MODE!!! Уровень безумия - " + str(self.madness) + " Случайно атакую -", target.name)
            print()
            self.attack(target)
        print('\n')


class MonsterHunter(Monster):

    def __init__(self, name):
        super().__init__(name)
        self.potions = 10

    def attack(self, target):
        target.take_damage(self.get_power() + (10 - self.potions))

    def take_damage(self, power):
        self.set_hp(self.get_hp() - power)
        if random.randint(1, 10) == 1:
            self.potions -= 1
        super().take_damage(power)

    def give_a_potion(self, target):
        self.potions -= 1
        target.set_hp(target.get_hp() + self.get_power())

    def make_a_move(self, friends, enemies):
        print(self.name, end=' ')
        target_of_potion = friends[0]
        min_health = target_of_potion.get_hp()
        for friend in friends:
            if friend.get_hp() < min_health:
                target_of_potion = friend
                min_health = target_of_potion.get_hp()

        if min_health < 60 and self.potions > 0:
            print("Исцеляю", target_of_potion.name)
            self.give_a_potion(target_of_potion)
        else:
            if not enemies:
                return
            print("Атакую ближнего -", enemies[0].name)
            self.attack(enemies[0])
        print('\n')
import random
from monsters import MonsterBerserk, MonsterHunter
from heroes import Tank, Healer, Attacker


def one_year_of_war():
    # Ниже приведен пример составления команды
    # Вы можете изменять состав команды, НО размер команды не должен быть более 5.

    tank = Tank("Танк Пётр")
    attacker = Attacker("Убийца Ольга")
    #second_attacker = Attacker("Убийца Траур")
    newhealer = Healer('Феофан Мудрый')
    healer = Healer("Монах Игнат")
    second_healer = Healer("Монах Ирэна")
    good_team = [tank, attacker, newhealer, second_healer, healer]

    # Код ниже изменять нельзя!

    # Функция запускает симуляцию одного года сражений.
    # В цикле запускается 365 итераций (1 итерация = 1 день)
    # Каждый день каждый герой и монстр выбирают и совершают ОДНО действие.
    # Если монстры умирают - они пропадают из списка
    # Если умирают герои - цикл завершается - битва считается проигранной (возвращается 0)
    # Если герои выживают - битва считается выигранной (возвращается 1)
    if sum([isinstance(hero, (MonsterHunter, MonsterBerserk)) for hero in good_team]) > 1:
        print("В команде героев может быть только 1 монстр!")
        return 0

    evil_names = ["Абвыргл", "Мефисто", "Драник", "Диабло", "Пусечка", "Стаут"]
    mob_warrior = MonsterBerserk("Берсерк " + random.choice(evil_names))
    mob_ranger = MonsterHunter("Рейнджер " + random.choice(evil_names))
    evil_team = [mob_warrior, mob_ranger]

    for day in range(1, 366):
        print("=" * 50 + "\nНачало дня №" + str(day) + "\n" + "=" * 50)

        # В циклах у героев и монстров вызывается метод make_a_move, который должен выбирать и совершать одно действие
        # Для наглядности вы можете добавлять в каждое действие принты с подробностями (чтобы знать кто когда и что совершает)
        # При помощи этой информации вы сможете искать проблемы и ошибки в вашем коде и в конечном итоге это поможет вам улучшить стратегию
        print("\nКоманда добра:\n" + '-' * 50)
        for hero in good_team:
            hero.make_a_move(good_team, evil_team)

        print("\nКоманда зла:\n" + '-' * 50)
        for mob in evil_team:
            mob.make_a_move(evil_team, good_team)

        print(f"Итоги дня сражений №{day}")

        # В итогах дня у каждого героя и каждого монстра вызывается метод __str__ который должен описывать их текущее состояние
        print("\nКоманда добра:\n" + '-' * 50)
        for hero in good_team:
            print(hero)

        print("\nКоманда зла:\n" + '-' * 50)
        for mob in evil_team:
            print(mob)

        # Мёртвые монстры удаляются из списка
        evil_team = [mob for mob in evil_team if mob.is_alive()]
        # Новые монстры в чётные дни добавляются в список (но их не может быть больше 4)
        if day % 2 == 0 and len(evil_team) < 4:
            newborn_evils = [MonsterBerserk("Берсерк " + random.choice(evil_names)), MonsterHunter("Рейнджер " + random.choice(evil_names))]
            evil_team.append(random.choice(newborn_evils))

        if any([not hero.is_alive() for hero in good_team]):
            print("Вы проиграли!")
            return 0
        else:
            print("Сражение продолжается!")

    else:
        print("Вы одержали победу!")
        return 1


# Код ниже не подлежит изменению
# Он запускает 20 симуляций. Для зачёта по заданию вам надо стабильно набирать 10 или более побед.
count_of_wins = 0
for year in range(1, 21):
    count_of_wins += one_year_of_war()

print("Из 20 раз команда героев одержала", count_of_wins, "побед")
if count_of_wins < 10:
    print("Героям нужна другая тактика, попробуйте ещё!")
else:
    print("Герои готовы к реальному сражению, задание выполнено!")