Создание бота для ролевой игры Discord с помощью Python

В этом уроке мы создадим текстовую ролевую игру, в которую пользователи сервера Discord смогут играть, вводя специальные команды. Пользователи смогут создавать персонажей, сражаться с врагами, зарабатывать опыт и золото, которые они смогут использовать для развития своих навыков и покупки мощных предметов.

К концу этого урока вы сможете:

  • Использовать файл discord.py для создания ролевой игры, которая может быть добавлена на любой сервер Discord и может быть расширена по вашему усмотрению.
  • Использовали Replit Database для хранения информации об игровом мире с помощью сериализации.
  • Создал пользовательскую вставку сообщений Discord.
  • Разместите своего собственного бота Discord на Replit!

Начало работы

Войдите в Replit или создайте учетную запись, если вы еще этого не сделали. После входа в систему создайте Python repl.

Дизайн игры

Прежде чем мы начнем писать код, стоит немного подумать о том, как будет работать наша игра и какие возможности мы предоставим нашим игрокам. Наша игра будет текстовой и будет проходить на сервере Discord. Игроки будут совершать действия, отдавая команды боту Discord, а бот будет отвечать результатами этих действий.

Игроки начнут игру с создания персонажа, которому они смогут дать имя. У этого персонажа будет следующее:

  • Хитпоинты, определяющие, сколько урона он может получить.
  • Навыки атаки и защиты, определяющие, как он действует в бою.
  • Уровень и очки опыта.
  • Мана для произнесения заклинаний.
  • Инвентарь для сбора предметов.
  • Золото для покупки предметов.

Персонажи будут получать опыт и золото за сражения и победы над врагами. Они смогут искать врагов и сражаться с ними. В режиме боя они смогут сражаться, использовать предмет или бежать, если не похоже, что они победят. Сражения будут продолжаться до тех пор, пока у одного из участников не закончатся хитпоинты, или пока персонаж игрока не сбежит.

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

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

Хотя мы включили в наш проект золото, ману и инвентарь, реализацию этих аспектов мы оставим на усмотрение читателя. В этом руководстве мы сосредоточимся на реализации сражений с врагами и повышении уровня.

Чтобы облегчить вышеперечисленное, нашей игре понадобятся два режима:

  • Режим приключения, в котором игроки могут охотиться на врагов и повышать уровень.
  • Режим битвы, в котором игроки могут сражаться с врагами.

Также нам понадобится несколько команд для выполнения игроками игровых действий:

Игровые классы

Прежде чем приступить к коду Discord, давайте создадим несколько классов для представления персонажей, врагов, режимов и предметов в нашей игре. В своем repl создайте новый файл с именем game.py и добавьте в него следующий код импорта:

from replit import db
import enum, random, sys
from copy import deepcopy
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы импортируем библиотеку Python для Replit Database, постоянного хранилища ключевых значений, подключаемого к каждому repl. Если вы не использовали его раньше, считайте, что это словарь Python с содержимым, которое сохраняется между перезапусками вашей реплики. Мы будем использовать его для хранения состояния нашей игры и ее персонажей, когда они сражаются с врагами и повышают уровень.

Кроме того, мы импортируем встроенные библиотеки Python enum, random, sys и deepcopy() из copy. Они предоставят ряд полезных утилит, которые мы будем использовать при создании нашей игры.

Во-первых, мы будем использовать enum.IntEnum для перечисления режимов игры. Добавьте следующий код ниже вашего импорта:

# Game modes
class GameMode(enum.IntEnum):
    ADVENTURE = 1
    BATTLE = 2
Вход в полноэкранный режим Выйти из полноэкранного режима

Всякий раз, когда мы будем ссылаться на игровой режим в нашем коде, мы сможем писать GameMode.ADVENTURE или GameMode.BATTLE, а не бессмысленные числа 1 или 2, но наш код и база данных будут видеть эти игровые режимы как 1 и 2. Это избавит нас от необходимости запоминать, какой режим игры какой, и сделает наш код более понятным.

Далее мы создадим классы для живых существ, таких как персонажи игроков и враги, с которыми они сражаются. Поскольку все они будут иметь несколько общих свойств и моделей поведения, таких как хитпоинты и способность сражаться, мы будем использовать наследование, чтобы избежать повторений. Иерархия классов будет выглядеть следующим образом (но с несколькими другими типами врагов):

       ,-----.             
       |Actor|             
       |-----|             
       `-----'             
         /                
        /                 
,---------.  ,-----.       
|Character|  |Enemy|       
|---------|  |-----|       
`---------'  `-----'       
              /           
      ,--------.   ,------.
      |GiantRat|   |Dragon|
      |--------|   |------|
      `--------'   `------'
Вход в полноэкранный режим Выход из полноэкранного режима

Начнем с реализации нашего родительского класса Actor. Этот класс определит все общие атрибуты персонажей и врагов и реализует метод fight(). Добавьте следующий код в нижней части game.py:

# Living creatures
class Actor:

    def __init__(self, name, hp, max_hp, attack, defense, xp, gold):
        self.name = name
        self.hp = hp
        self.max_hp = max_hp
        self.attack = attack
        self.defense = defense
        self.xp = xp
        self.gold = gold

    def fight(self, other):
        defense = min(other.defense, 19) # cap defense value
        chance_to_hit = random.randint(0, 20-defense)
        if chance_to_hit: 
            damage = self.attack
        else:
            damage = 0

        other.hp -= damage

        return (self.attack, other.hp <= 0) #(damage, fatal)
Вход в полноэкранный режим Выход из полноэкранного режима

Наш метод __init__() определяет несколько переменных в соответствии со спецификацией дизайна игры, приведенной выше. Обратите внимание, что мы определили hp и max_hp: они должны иметь одинаковое значение при первом создании класса персонажа или врага, но будут отличаться для персонажей и врагов, которых мы считываем из базы данных. Когда мы перейдем к игровой логике, мы будем постоянно воссоздавать экземпляры этих классов из базы данных.

Также обратите внимание, что self.xp будет представлять нечто немного разное для персонажей и врагов: Для персонажей это будут суммарные заработанные очки опыта, в то время как для врагов это будет количество XP, начисляемое персонажам после победы над врагом. Более сложная конструкция может вместо этого позволить врагам получать очки опыта и повышать уровень, как персонажи игрока.

Метод fight() принимает other, экземпляр класса, который наследуется от Actor. Атака моделируется путем вычисления шанса на попадание на основе атрибута defense противника. Если chance_to_hit равен 0, атака будет пропущена. Вероятность промаха увеличивается по мере увеличения значения defense. Например, атака будет иметь 95% вероятность успеха против врага с defense равным 1, но только 50% вероятность успеха против врага с defense равным 19. Мы используем встроенную в Python функцию min() для ограничения значения defense на 19, чтобы избежать создания полностью неуязвимого персонажа.

Урон, наносимый при успешном попадании, определяется атрибутом Actor attack. Функция возвращает кортеж из количества урона, нанесенного атакой, и того, был ли этот удар смертельным.

На этом наш класс Actor завершен. Давайте создадим класс Character, который наследуется от него и будет представлять игроков. Добавьте следующий код в нижнюю часть файла game.py:

class Character(Actor):

    level_cap = 10

    def __init__(self, name, hp, max_hp, attack, defense, mana, level, xp, gold, inventory, mode, battling, user_id):
        super().__init__(name, hp, max_hp, attack, defense, xp, gold)
        self.mana = mana
        self.level = level

        self.inventory = inventory 

        self.mode = mode
        self.battling = battling
        self.user_id = user_id
Войти в полноэкранный режим Выйти из полноэкранного режима

Наш класс Character имеет все те же атрибуты, что и Actor, плюс некоторые дополнительные:

Также обратите внимание на переменную класса level_cap: это будет максимально возможное значение level.

Мы вернемся к классу Character и используем его для реализации игровых действий позже, но сначала давайте определим классы врагов, начиная с Enemy.

class Enemy(Actor):

    def __init__(self, name, max_hp, attack, defense, xp, gold):
        super().__init__(name, max_hp, max_hp, attack, defense, xp, gold)
Вход в полноэкранный режим Выход из полноэкранного режима

Пока это простой подкласс Actor, который использует те же значения для hp и max_hp, но не определяет ничего дополнительного. Фактические статы отдельных типов врагов мы определим в подклассах Enemy, которых у нас будет десять. Скопируйте код, приведенный ниже, в нижнюю часть game.py для реализации этих классов:

class GiantRat(Enemy):
    min_level = 1
    def __init__(self):
        super().__init__("🐀 Giant Rat", 2, 1, 1, 1, 1) # HP, attack, defense, XP, gold

class GiantSpider(Enemy):
    min_level = 1
    def __init__(self):
        super().__init__("🕷️ Giant Spider", 3, 2, 1, 1, 2) # HP, attack, defense, XP, gold

class Bat(Enemy):
    min_level = 1
    def __init__(self):
        super().__init__("🦇 Bat", 4, 2, 1, 2, 1) # HP, attack, defense, XP, gold

class Crocodile(Enemy):
    min_level = 2
    def __init__(self):
        super().__init__("🐊 Crocodile", 5, 3, 1, 2, 2) # HP, attack, defense, XP, gold

class Wolf(Enemy):
    min_level = 2
    def __init__(self):
        super().__init__("🐺 Wolf", 6, 3, 2, 2, 2) # HP, attack, defense, XP, gold

class Poodle(Enemy):
    min_level = 3
    def __init__(self):
        super().__init__("🐩 Poodle", 7, 4, 1, 3, 3) # HP, attack, defense, XP, gold

class Snake(Enemy):
    min_level = 3
    def __init__(self):
        super().__init__("🐍 Snake", 8, 4, 2, 3, 3) # HP, attack, defense, XP, gold

class Lion(Enemy):
    min_level = 4
    def __init__(self):
        super().__init__("🦁 Lion", 9, 5, 1, 4, 4) # HP, attack, defense, XP, gold

class Dragon(Enemy):
    min_level = 5
    def __init__(self):
        super().__init__("🐉 Dragon", 10, 6, 2, 5, 5) # HP, attack, defense, XP, gold
Вход в полноэкранный режим Выход из полноэкранного режима

Помимо имен и жестко закодированных значений HP, атаки, защиты, XP и золота, мы ввели min_level в качестве переменной класса. Она определяет минимальный уровень, на котором должен быть игрок, чтобы встретиться с этим врагом. Благодаря этому мы избегаем мгновенной смерти игроков низкого уровня против слишком сильных врагов и гарантируем, что новые враги будут появляться по мере роста уровня игрока, создавая ощущение развития. Не стесняйтесь изменять любого из этих врагов или добавлять своих собственных.

Сохранение и загрузка из базы данных

Теперь мы определили классы игрока и врага, хотя они мало что делают. Но прежде чем мы реализуем какой-либо реальный игровой процесс, нам нужен способ обеспечить сохранение состояния наших персонажей и врагов от одного момента к другому. В настоящее время все экземпляры Actor, которые мы создаем, исчезают, как только наш repl завершает работу или останавливается. Нам нужно использовать базу данных нашего repl для того, чтобы игроки могли сохранять созданных ими персонажей и чтобы такие изменения, как потеря и набор HP и повышение уровня, сохранялись.

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

Вернитесь к определению Character и добавьте следующий метод save_to_db() под методом __init__():

class Character(Actor):

    # ...

    def save_to_db(self):
        character_dict = deepcopy(vars(self))
        if self.battling != None:
            character_dict["battling"] = deepcopy(vars(self.battling))

        db["characters"][self.user_id] = character_dict
Вход в полноэкранный режим Выход из полноэкранного режима

В верхней части game.py мы импортировали db из библиотеки replit Python — этот объект предоставляет интерфейс к базе данных нашего repl. Объект db предназначен для использования как словарь, поэтому мы можем создавать ключи и значения, как и в любом другом словаре. Схема нашей базы данных будет выглядеть следующим образом:

{
    "characters": {
        "123456789012345678": {
            "name": "Bob the Dwarf"
            "hp": 10,
            ...
        },
        "823486788042375673": {
            "name": "Eric the Human"
            "hp": 8,
            ...
        },
        ...
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Функция vars() — это встроенная функция Python, которая возвращает значение __dict__ для любого класса, модуля или экземпляра, который мы ей передаем. Для большинства экземпляров это будет словарь, содержащий атрибуты данного экземпляра. В случае нашего объекта Character, словарь будет содержать все атрибуты, которые мы определили в __init__. Мы используем deepcopy() для создания полной копии этого словаря.

Любые атрибуты, содержащие строки, числа, булевы значения или даже списки или словари, могут быть легко и осмысленно сохранены в базе данных нашего repl. Атрибуты, которые ссылаются на экземпляры наших пользовательских классов, не могут быть сохранены с пользой, так как при следующей загрузке данных этот экземпляр может не существовать. Операция deepcopy() сама по себе не решает эту проблему. Таким образом, нам нужно хранить объект, на который ссылается battling, как словарь его атрибутов с помощью vars(), точно так же, как мы это делали для экземпляра Character.

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

class Enemy(Actor):

    def __init__(self, name, max_hp, attack, defense, xp, gold):
        super().__init__(name, max_hp, max_hp, attack, defense, xp, gold)
        # NEW LINE BELOW
        self.enemy = self.__class__.__name__
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта новая строка будет хранить имя класса в виде строки, которую мы можем записать в базу данных.

Теперь мы написали весь код, необходимый для сохранения персонажей игроков в базе данных. Теперь нам нужен способ загрузить их обратно в игру. К счастью, большая часть того, что нам нужно для этого, уже есть в нашем методе Character.__init__(). Следующая строка кода инициализирует новый Character с данными из нашей базы данных:

Character(**db["characters"]["123456789012345678"])
Войти в полноэкранный режим Выйти из полноэкранного режима

Единственная проблема здесь заключается в том, что значением battling будет словарь, а не подкласс Enemy. Мы можем исправить это, внеся некоторые изменения в Character.__init__(). Найдите этот метод и замените строку self.battling = battling новым кодом, приведенным ниже:

class Character(Actor):

    level_cap = 10

    def __init__(self, name, hp, max_hp, attack, defense, mana, level, xp, gold, inventory, mode, battling, user_id):
        super().__init__(name, hp, max_hp, attack, defense, xp, gold)
        self.mana = mana
        self.level = level

        self.inventory = inventory 

        self.mode = mode
        # NEW CODE BELOW THIS LINE
        if battling != None:
            enemy_class = str_to_class(battling["enemy"])
            self.battling = enemy_class()
            self.battling.rehydrate(**battling)
        else:
            self.battling = None
        # NEW CODE ABOVE THIS LINE

        self.user_id = user_id
Вход в полноэкранный режим Выйти из полноэкранного режима

Этот код преобразует значение атрибута enemy, который мы создали выше, из строки в класс, инициализирует копию этого класса, а затем вызывает rehydrate, распаковывая словарь battling в качестве аргументов. Мы напишем функцию str_to_class и метод Enemy.rehydrate() в ближайшее время.

Функция str_to_class будет принимать строку и возвращать класс с его именем.

Метод rehydrate установит все атрибуты экземпляра в соответствии с предоставленными. Хотя мы могли бы сделать это с помощью метода __init__(), как мы делали с Character, это заставило бы нас указывать все значения атрибутов каждый раз, когда мы инициализируем любой подкласс Enemy, что сводит на нет смысл наличия подклассов в первую очередь.

Перейдите в начало game.py и создайте функцию str_to_class чуть ниже вашего импорта, как показано ниже:

# Helper functions
def str_to_class(classname):
    return getattr(sys.modules[__name__], classname)
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта функция использует полезную встроенную функцию Python getattr для получения класса, соответствующего строке, указанной как classname. Обратите внимание, что эта функция будет работать только для определенных нами классов.

Далее вернитесь к классу Enemy и создайте метод rehydrate() прямо под методом __init__().

class Enemy(Actor):

    def __init__(self, name, max_hp, attack, defense, xp, gold):
        super().__init__(name, max_hp, max_hp, attack, defense, xp, gold)
        self.enemy = self.__class__.__name__

    # NEW METHOD
    def rehydrate(self, name, hp, max_hp, attack, defense, xp, gold, enemy):
        self.name = name
        self.hp = hp
        self.max_hp = max_hp
        self.attack = attack
        self.defense = defense
        self.xp = xp
        self.gold = gold
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что мы принимаем enemy в качестве аргумента, не используя его. Это сделано для предотвращения ошибок при распаковке словаря battling.

Теперь мы сможем сохранять персонажей в базу данных, вызывая save_to_db(), и загружать персонажей из базы данных, передавая запись базы данных в Character(). Поскольку наши персонажи хранят информацию о врагах, с которыми они сражаются, это все, что нам нужно для постоянного игрового мира.

Игровые действия

Давайте реализуем действия игрока из нашей спецификации дизайна. Начнем с охоты — действия, которое позволяет персонажам искать врагов для сражения. Добавьте следующий код в определение класса Character:

    def hunt(self):
        # Generate random enemy to fight
        while True:
            enemy_type = random.choice(Enemy.__subclasses__())

            if enemy_type.min_level <= self.level:
                break

        enemy = enemy_type()

        # Enter battle mode
        self.mode = GameMode.BATTLE
        self.battling = enemy

        # Save changes to DB after state change
        self.save_to_db()

        return enemy
Вход в полноэкранный режим Выход из полноэкранного режима

Сначала мы используем random.choice() для случайного выбора одного из подклассов Enemy. Этот случайный выбор будет повторяться до тех пор, пока мы не выберем врага с минимальным уровнем, меньшим или равным уровню персонажа игрока.

Как только враг выбран, мы инициализируем его экземпляр, переключаем режим игры и сохраняем ссылку на него в battling. Затем мы обновляем объект игрока в базе данных и возвращаем объект врага.

Нам нужно будет вызывать save_to_db() в конце каждого метода, который изменяет состояние персонажа. Это включает метод fight(), определенный в Actor. Для этого добавьте следующий метод в класс Character:

   def fight(self, enemy):
        outcome = super().fight(enemy)

        # Save changes to DB after state change
        self.save_to_db()

        return outcome
Войти в полноэкранный режим Выйти из полноэкранного режима

Этот метод будет вызывать Actor.fight(), сохранять результат, обновлять базу данных, а затем возвращать результат.

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

    def flee(self, enemy):
        if random.randint(0,1+self.defense): # flee unscathed
            damage = 0
        else: # take damage
            damage = enemy.attack/2
            self.hp -= damage

        # Exit battle mode
        self.battling = None
        self.mode = GameMode.ADVENTURE

        # Save to DB after state change
        self.save_to_db()

        return (damage, self.hp <= 0) #(damage, killed)
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтобы добавить некоторую неопределенность в действие бегства, а также дополнительное использование стата защиты, мы реализовали случайный шанс того, что игрок получит небольшое количество урона при бегстве. Затем мы опустошаем battling, меняем режим игры, сохраняем состояние и возвращаем кортеж результатов действия, аналогичный тому, который возвращается в Actor.fight().

Далее нам понадобится метод, который будет вызываться после победы над врагом, чтобы выдать персонажу золото и XP. Добавьте следующее определение метода:

    def defeat(self, enemy):
        if self.level < self.level_cap: # no more XP after hitting level cap
            self.xp += enemy.xp

        self.gold += enemy.gold # loot enemy

        # Exit battle mode
        self.battling = None
        self.mode = GameMode.ADVENTURE

        # Check if ready to level up after earning XP
        ready, _ = self.ready_to_level_up()

        # Save to DB after state change
        self.save_to_db()

        return (enemy.xp, enemy.gold, ready)
Войти в полноэкранный режим Выйти из полноэкранного режима

Если уровень игрока ниже предела, мы добавим XP врага к его собственному. Затем мы добавляем золото врага в кошелек, выходим из режима боя и проверяем, набрал ли игрок достаточно XP для повышения уровня (с помощью метода, который мы реализуем позже). Наконец, мы сохраняем состояние персонажа в базе данных и возвращаем кортеж результатов этого действия.

Далее определим ready_to_level_up():

    def ready_to_level_up(self):
        if self.level == self.level_cap: # zero values if we've ready the level cap
            return (False, 0)

        xp_needed = (self.level)*10
        return (self.xp >= xp_needed, xp_needed-self.xp) #(ready, XP needed)
Вход в полноэкранный режим Выход из полноэкранного режима

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

Теперь, когда мы увеличиваем XP игрока и проверяем, готов ли он к повышению уровня, нам нужен метод для повышения уровня. Добавьте следующий метод:

    def level_up(self, increase):
        ready, _ = self.ready_to_level_up()
        if not ready:
            return (False, self.level) # (not leveled up, current level)

        self.level += 1 # increase level
        setattr(self, increase, getattr(self, increase)+1) # increase chosen stat

        self.hp = self.max_hp #refill HP

        # Save to DB after state change
        self.save_to_db()

        return (True, self.level) # (leveled up, new level)
Войти в полноэкранный режим Выйти из полноэкранного режима

Убедившись, что игрок готов к повышению уровня, мы повышаем его уровень и используем встроенные функции Python setattr и getattr для увеличения одной из статистик персонажа. Мы сбрасываем их HP до максимального значения, сохраняем состояние и, наконец, возвращаем результат действия (кортеж, указывающий, удалось ли повысить уровень и каков новый уровень персонажа).

Последний метод, который нам нужен, это die(), который будет вызываться, когда персонаж будет побежден в бою. Мы можем обрабатывать смерть игрока несколькими различными способами, но для простоты мы просто удалим персонажа из базы данных.

    def die(self, player_id):
        if self.user_id in db["characters"].keys():
            del db["characters"][self.user_id]
Вход в полноэкранный режим Выход из полноэкранного режима

На этом логика нашей игры закончена. Далее мы интегрируемся с Discord и сделаем нашу игру играбельной.

Создание приложения Discord

Discord будет служить слоем отображения для нашей игры, позволяя игрокам отдавать команды и видеть результаты этих команд.

Откройте другую вкладку браузера и посетите портал разработчиков Discord. Войдите в систему под своей учетной записью Discord или создайте ее, если вы еще не сделали этого. Держите ответ открытым — мы скоро к нему вернемся.

Как только вы войдете в систему, создайте новое приложение. Дайте ему имя, например «MyRPG».

Приложения Discord могут взаимодействовать с Discord несколькими различными способами, не все из которых требуют ботов, поэтому создавать их необязательно. Тем не менее, для этого проекта он нам понадобится. Давайте создадим бота.

  1. Нажмите на Bot в меню в левой части страницы.
  2. Нажмите Добавить бота.
  3. Дайте боту имя пользователя (например, «RPGBot»).
  4. Нажмите Сбросить токен, а затем Да, сделайте это!
  5. Скопируйте токен, который появится сразу под именем пользователя вашего бота.

Токен, который вы только что скопировали, необходим для того, чтобы код в нашей реплике мог взаимодействовать с API Discord. Вернитесь к своему боту и откройте вкладку Secrets в левой боковой панели. Создайте новый секрет с DISCORD_TOKEN в качестве ключа и скопированным вами токеном в качестве значения.

Как только вы это сделаете, вернитесь в панель разработчика Discord. Нам нужно закончить настройку нашего бота.

Вы можете оставить опцию Public Bot включенной или отключить ее, в зависимости от того, хотите ли вы, чтобы другие люди могли найти и установить вашего бота на своем сервере. Имейте в виду, что боты на 100 и более серверах должны пройти специальный процесс проверки и одобрения.

Далее нам нужно настроить доступ к привилегированным намерениям шлюза. В зависимости от функциональности бота, ему потребуется доступ к различным событиям и источникам данных. События, связанные с действиями пользователей и содержанием их сообщений, считаются более конфиденциальными и требуют явного разрешения.

Для работы этого бота нам понадобится Message Content Intent, который позволит нашему боту видеть содержание сообщений пользователей. Переключите его в положение on и сохраните изменения, когда появится запрос.

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

Откройте Discord в своем браузере. Вы уже должны быть авторизованы. Затем нажмите на значок + в крайней левой панели, чтобы создать новый сервер. Или же откройте существующий сервер, которым вы владеете.

В отдельной вкладке вернитесь на портал Discord Dev Portal и откройте свое приложение. Затем выполните следующие шаги, чтобы добавить бота на свой сервер:

  1. Нажмите на OAuth2 в левой боковой панели.
  2. В появившемся меню под OAuth2 выберите URL Generator.
  3. В разделе Scopes отметьте чекбокс с надписью bot.
  4. В разделе «Разрешения бота» отметьте чекбоксы «Читать сообщения/Просматривать каналы» и «Отправлять сообщения».

  5. Прокрутите вниз и скопируйте URL-адрес в разделе Generated URL.

  6. Вставьте URL-адрес в навигационную панель браузера и нажмите кнопку Enter.

  7. На появившейся странице выберите свой сервер из выпадающего списка и нажмите Продолжить.

  8. Когда появится запрос о разрешениях, нажмите Авторизация и заполните CAPTCHA.

  9. Вернитесь на свой сервер Discord. Вы должны увидеть, что ваш бот только что присоединился.

Теперь, когда мы выполнили подготовительную работу, пришло время написать код. Вернитесь к своему repl для следующего раздела.

Написание кода бота Discord

Мы будем использовать discord.py для взаимодействия с API Discord с помощью Python. Откройте main.py в вашем repl и добавьте следующий код:

import os, discord
from discord.ext import commands

from replit import db
from game import *

DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")

bot = commands.Bot(command_prefix="!")

@bot.event
async def on_ready():
    print(f"{bot.user} has connected to Discord!")

bot.run(DISCORD_TOKEN)
Войти в полноэкранный режим Выйти из полноэкранного режима

Сначала мы импортируем необходимые нам библиотеки Python, включая discord.py и его расширение commands, а также нашу базу данных и содержимое game.py.

Затем мы получаем значение переменной окружения DISCORD_TOKEN, которую мы установили на вкладке Secrets нашего repl выше. После этого мы создаем объект Bot. Мы будем использовать этот объект для прослушивания событий Discord и реагирования на них. По большей части мы будем отвечать на команды: сообщения от пользователей, которые начинаются с ! (command_prefix, который мы указали при создании объекта Bot).

Однако первое интересующее нас событие не является командой. Событие on_ready() сработает, когда наш бот войдет в Discord (декоратор @bot.event обеспечивает это). Все, что сделает это событие, это выведет сообщение в консоль нашего repl, сообщая нам, что бот подключился.

Обратите внимание, что мы добавили async к определению функции — это превращает нашу функцию on_ready() в корутину. Корутины во многом похожи на функции, но могут выполняться не сразу, а должны вызываться с помощью ключевого слова await. Использование coroutines делает нашу программу асинхронной, то есть она может продолжать выполнять код, ожидая результатов долго выполняющейся функции, обычно зависящей от ввода или вывода. Если вы уже использовали JavaScript, вы узнаете этот стиль программирования.

Последняя строка в нашем файле запускает бота, предоставляя DISCORD_TOKEN для его аутентификации. Запустите свою реплику сейчас, чтобы увидеть ее в действии. После запуска вернитесь на свой сервер Discord. Вы должны увидеть, что ваш пользователь-бот теперь онлайн.

Обработка команд пользователя

Теперь мы можем начать писать обработчики для действий нашей игры, таких как !create, !hunt и !fight.

Расширение discord.py commands позволяет нам определять обработчики команд с помощью декоратора @bot.command. Без этого нам пришлось бы вручную анализировать содержимое всех сообщений пользователя, чтобы определить, была ли отдана команда, как это было необходимо в нашем учебном пособии по распределению ролей.

Создание персонажа

Сначала мы реализуем команду создания персонажа, !create. Добавьте следующий код в main.py ниже определения on_ready():

# Commands
@bot.command(name="create", help="Create a character.")
async def create(ctx, name=None):
    user_id = ctx.message.author.id

    # if no name is specified, use the creator's nickname
    if not name:
        name = ctx.message.author.name

Войти в полноэкранный режим Выход из полноэкранного режима

Декоратор @bot.command обеспечит вызов нашей функции, когда пользователь набирает сообщение, начинающееся с !create. Мы также используем его для определения текста помощи — расширение commands предоставляет команду по умолчанию !help, и каждая команда, которую мы определяем, может иметь два типа поясняющего текста:

В отсутствие brief, текст help будет использоваться в обоих случаях, хотя он может быть усечен для вывода !help.

Наша функция create принимает два параметра:

Тело функции извлекает идентификатор пользователя Discord, который выдал команду !create. Затем проверяется, был ли указан параметр name. Если нет, он устанавливает name в имя пользователя.

Далее мы создадим экземпляр Character с некоторыми начальными статистическими данными и сохраним его в базе данных нашего repl. Добавьте следующий код в тело create:

    # create characters dictionary if it does not exist
    if "characters" not in db.keys():
        db["characters"] = {}

    # only create a new character if the user does not already have one
    if user_id not in db["characters"] or not db["characters"][user_id]:
        character = Character(**{
            "name": name,
            "hp": 16,
            "max_hp": 16,
            "attack": 2,
            "defense": 1,
            "mana": 0,
            "level": 1,
            "xp": 0,
            "gold": 0,
            "inventory": [],
            "mode": GameMode.ADVENTURE,
            "battling": None,
            "user_id": user_id
        })
        character.save_to_db()
        await ctx.message.reply(f"New level 1 character created: {name}. Enter `!status` to see your stats.")
    else:
        await ctx.message.reply("You have already created your character.")
Войти в полноэкранный режим Выйти из полноэкранного режима

После создания и сохранения нового персонажа или в случае неудачи этот код отправляет ответ на вызвавшее его сообщение. Поскольку наша игровая логика в основном находится в файле game.py, создание этих команд будет в основном заключаться в создании и отправке ответов, информирующих игрока о том, что произошло в игровом мире.

Состояние персонажа

Далее мы реализуем команду !status, которую игроки будут использовать для просмотра текущей статистики, инвентаря и режима игры своего персонажа. Чтобы передать эту информацию компактно и привлекательно, мы будем использовать вставку, а не обычное сообщение Discord.

Embeds обычно используются для предварительного просмотра ссылок, но их также можно создавать с нуля, предоставляя ботам мощный инструмент для отображения богато отформатированной информации любого вида. Вот как будет выглядеть наш эмбед:

Добавьте следующую функцию ниже определения create():

@bot.command(name="status", help="Get information about your character.")
async def status(ctx):
    character = load_character(ctx.message.author.id)

    embed = status_embed(ctx, character)
    await ctx.message.reply(embed=embed)
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта функция извлекает персонажа игрока из базы данных, передает его и текущий контекст функции, которая создаст и вернет embed, а затем ответит с embed. Мы реализуем два метода, которые мы использовали чуть выше определения on_ready(). Перейдите к ним.

Во-первых, load_character(), который считывает данные из базы данных и создает экземпляр Character, используя результаты:

# Helper functions
def load_character(user_id):
    return Character(**db["characters"][str(user_id)])
Вход в полноэкранный режим Выйти из полноэкранного режима

Во-вторых, status_embed():

MODE_COLOR = {
    GameMode.BATTLE: 0xDC143C,
    GameMode.ADVENTURE: 0x005EB8,
}
def status_embed(ctx, character):

    # Current mode
    if character.mode == GameMode.BATTLE:
        mode_text = f"Currently battling a {character.battling.name}."
    elif character.mode == GameMode.ADVENTURE:
        mode_text = "Currently adventuring."

    # Create embed with description as current mode
    embed = discord.Embed(title=f"{character.name} status", description=mode_text, color=MODE_COLOR[character.mode])
    embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.avatar_url)
Войти в полноэкранный режим Выход из полноэкранного режима

Чуть выше определения функции мы создали словарь, который сопоставляет режимы игры с цветами. Мы будем использовать его для изменения цвета вертикальной полосы в левой части вставки.

В самой функции мы сначала проверяем режим игры. Это определит текст описания вставки, который появится сразу под заголовком вставки. После этого мы создаем вставку с помощью discord.Embed, устанавливая заголовок, описание и цвет. Затем мы используем set_author(), чтобы включить имя вызывающего пользователя и его фотографию профиля в верхней части вставки.

Далее мы создадим поля. Их можно представить как отдельные текстовые поля, которые будут отображаться под описанием. Мы начнем с поля статистики:

    # Stats field
    _, xp_needed = character.ready_to_level_up()

    embed.add_field(name="Stats", value=f"""
**HP:**    {character.hp}/{character.max_hp}
**ATTACK:**   {character.attack}
**DEFENSE:**   {character.defense}
**MANA:**  {character.mana}
**LEVEL:** {character.level}
**XP:**    {character.xp}/{character.xp+xp_needed}
    """, inline=True)
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы использовали add_field() для создания поля с заголовком «Stats», которое содержит список всех важных статистических данных игрока. Обратите внимание на вызов character.ready_to_level_up(), чтобы мы могли показать игроку, сколько XP ему нужно для перехода на следующий уровень. Мы также установили inline=True, что позволяет нам отображать поля в виде колонок.

В следующем столбце будет показан инвентарь игрока:

    # Inventory field
    inventory_text = f"Gold: {character.gold}n"
    if character.inventory:
        inventory_text += "n".join(character.inventory)

    embed.add_field(name="Inventory", value=inventory_text, inline=True)
Войти в полноэкранный режим Выход из полноэкранного режима

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

Наконец, мы вернем вставку.

    return embed 
Вход в полноэкранный режим Выход из полноэкранного режима

Запустите свой repl сейчас, а затем переключите вкладку на ваш сервер Discord. Создайте персонажа командой !create и просмотрите его статус командой !status.

Сражения

Далее давайте реализуем наши боевые команды, начиная с !hunt. Добавьте следующее определение функции под телом status():

@bot.command(name="hunt", help="Look for an enemy to fight.")
async def hunt(ctx):
    character = load_character(ctx.message.author.id)

    if character.mode != GameMode.ADVENTURE:
        await ctx.message.reply("Can only call this command outside of battle!")
        return

    enemy = character.hunt()

    # Send reply
    await ctx.message.reply(f"You encounter a {enemy.name}. Do you `!fight` or `!flee`?")
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта функция довольно проста: Мы загружаем персонажа, убеждаемся, что он не находится в данный момент в бою, вызываем hunt() для генерации случайного врага и отвечаем игроку сообщением о том, с кем он сражается и какие команды он может использовать.

Далее мы реализуем !fight:

@bot.command(name="fight", help="Fight the current enemy.")
async def fight(ctx):
    character = load_character(ctx.message.author.id)

    if character.mode != GameMode.BATTLE:
        await ctx.message.reply("Can only call this command in battle!")
        return

    # Simulate battle
    enemy = character.battling

    # Character attacks
    damage, killed = character.fight(enemy)
    if damage:
        await ctx.message.reply(f"{character.name} attacks {enemy.name}, dealing {damage} damage!")
    else:
        await ctx.message.reply(f"{character.name} swings at {enemy.name}, but misses!")
Войти в полноэкранный режим Выход из полноэкранного режима

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

Далее нам нужно проверить, был ли враг убит атакой. Добавьте следующий код в конец функции:

    # End battle in victory if enemy killed
    if killed:
        xp, gold, ready_to_level_up = character.defeat(enemy)

        await ctx.message.reply(f"{character.name} vanquished the {enemy.name}, earning {xp} XP and {gold} GOLD. HP: {character.hp}/{character.max_hp}.")

        if ready_to_level_up:
            await ctx.message.reply(f"{character.name} has earned enough XP to advance to level {character.level+1}. Enter `!levelup` with the stat (HP, ATTACK, DEFENSE) you would like to increase. e.g. `!levelup hp` or `!levelup attack`.")

        return
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь мы вызываем character.defeat(), чтобы обработать смерть врага и вернуть соответствующие ответы. Опять же, мы уже написали всю игровую логику, поэтому все, что нужно сделать этому коду, это отобразить ее игроку. Как только мы отправили ответ, мы возвращаемся из функции.

После того как персонаж игрока атакует, нам нужно, чтобы враг дал отпор. Добавьте следующий код ниже блока if killed:

    # Enemy attacks
    damage, killed = enemy.fight(character)
    if damage:
        await ctx.message.reply(f"{enemy.name} attacks {character.name}, dealing {damage} damage!")
    else:
        await ctx.message.reply(f"{enemy.name} tries to attack {character.name}, but misses!")

    character.save_to_db() #enemy.fight() does not save automatically
Войти в полноэкранный режим Выйти из полноэкранного режима

Это почти идентично коду атаки игрока, но с enemy.fight(character) вместо character.fight(enemy). Но поскольку enemy.fight() не сохраняется в базе данных после изменения состояния игры, мы должны сделать это вручную.

Далее нам нужен код для проверки того, был ли персонаж игрока убит во время атаки. Добавьте следующие строки в вашу функцию:

    # End battle in death if character killed
    if killed:
        character.die()

        await ctx.message.reply(f"{character.name} was defeated by a {enemy.name} and is no more. Rest in peace, brave adventurer.")
        return
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы удаляем персонажа из базы данных, отправляем сообщение с соболезнованиями и возвращаемся из функции.

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

    # No deaths, battle continues
    await ctx.message.reply(f"The battle rages on! Do you `!fight` or `!flee`?")
Вход в полноэкранный режим Выход из полноэкранного режима

Это все для !fight — теперь нам нужно !flee! Добавьте следующую функцию ниже той, которую вы только что закончили:

@bot.command(name="flee", help="Flee the current enemy.")
async def flee(ctx):
    character = load_character(ctx.message.author.id)

    if character.mode != GameMode.BATTLE:
        await ctx.message.reply("Can only call this command in battle!")
        return

    enemy = character.battling
    damage, killed = character.flee(enemy)

    if killed:
        character.die()
        await ctx.message.reply(f"{character.name} was killed fleeing the {enemy.name}, and is no more. Rest in peace, brave adventurer.")
    elif damage:
        await ctx.message.reply(f"{character.name} flees the {enemy.name}, taking {damage} damage. HP: {character.hp}/{character.max_hp}")
    else:
        await ctx.message.reply(f"{character.name} flees the {enemy.name} with their life but not their dignity intact. HP: {character.hp}/{character.max_hp}")
Войти в полноэкранный режим Выйти из полноэкранного режима

И снова эта функция загружает персонажа, проверяет, что режим игры соответствует вызываемой команде, а затем вызывает соответствующий метод в Character. Мы завершаем функцию, предусмотрев три возможных исхода попытки бегства: персонаж умирает, персонаж бежит, получая урон, и персонаж бежит невредимым.

Повторно запустите свою реплику и попробуйте поохотиться, сразиться и убежать.

Повышение уровня

Далее нам нужно реализовать !levelup. Добавьте следующий код ниже определения !flee:

@bot.command(name="levelup", help="Advance to the next level. Specify a stat to increase (HP, ATTACK, DEFENSE).")
async def levelup(ctx, increase):
    character = load_character(ctx.message.author.id)

    if character.mode != GameMode.ADVENTURE:
        await ctx.message.reply("Can only call this command outside of battle!")
        return

    ready, xp_needed = character.ready_to_level_up()
    if not ready:
        await ctx.message.reply(f"You need another {xp_needed} to advance to level {character.level+1}")
        return

    if not increase:
        await ctx.message.reply("Please specify a stat to increase (HP, ATTACK, DEFENSE)")
        return
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта функция принимает increase, которая будет строкой, содержащей стат для увеличения. После стандартной загрузки персонажа и проверки режима мы выполняем некоторую обработку ошибок. Во-первых, мы отклоняем команду, если у персонажа недостаточно XP для повышения уровня, а затем отклоняем команду, если игрок не указал стат для повышения.

Далее нам нужно разобрать значение increase. Добавьте следующий код в вашу функцию:

    increase = increase.lower()
    if increase == "hp" or increase == "hitpoints" or increase == "max_hp" or increase == "maxhp":
        increase = "max_hp"
    elif increase == "attack" or increase == "att":
        increase = "attack"
    elif increase == "defense" or increase == "def" or increase == "defence":
        increase = "defense"
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы разрешаем игрокам увеличивать только статы HP, атаки или защиты своих персонажей. Чтобы сделать нашу игру максимально удобной для пользователя, мы принимаем несколько разных слов для каждого из этих показателей.

Наконец, мы вызываем метод level_up() персонажа и сообщаем о его результатах:

    success, new_level = character.level_up(increase)
    if success:
        await ctx.message.reply(f"{character.name} advanced to level {new_level}, gaining 1 {increase.upper().replace('_', ' ')}.")
    else:
        await ctx.message.reply(f"{character.name} failed to level up.")
Вход в полноэкранный режим Выход из полноэкранного режима

Повторно запустите свою игру и проверьте это. Если вы предпочитаете избегать шлифовки, временно измените код создания персонажа, чтобы увеличить начальное количество XP.

Смерть персонажа

В нашем техническом задании есть еще одна команда, которую мы пока не реализовали: !die. Игроки будут использовать эту команду, если захотят начать игру заново с новым персонажем. Добавьте следующий код для ее определения:

@bot.command(name="die", help="Destroy current character.")
async def die(ctx):
    character = load_character(ctx.message.author.id)

    character.die()

    await ctx.message.reply(f"Character {character.name} is no more. Create a new one with `!create`.")
Войти в полноэкранный режим Выйти из полноэкранного режима

Сброс персонажа

Прежде чем закончить, мы собираемся реализовать последнюю, специальную команду: !reset. Эта команда удалит персонажа игрока и тут же создаст нового персонажа. В отличие от команд выше, это будет тестовая команда, предназначенная для использования разработчиком, а не игроками. Добавьте следующий код ниже определения die():

@bot.command(name="reset", help="[DEV] Destroy and recreate current character.")
async def reset(ctx):
    user_id = str(ctx.message.author.id)

    if user_id in db["characters"].keys():
        del db["characters"][user_id]

    await ctx.message.reply(f"Character deleted.")
    await create(ctx)
Войти в полноэкранный режим Выйти из полноэкранного режима

В отличие от die(), мы удаляем из базы данных напрямую, а не используем метод character.die(). Это полезно, поскольку дальнейшее развитие игры может привести к ошибкам в Character.__init__(), что сделает метод die() временно непригодным.

Что дальше?

Мы создали текстовую ролевую игру, в которую можно играть на сервере Discord, но наша игра довольно пустая. Мы можем расширить ее несколькими различными способами:

  • Реализовать систему магии, используя атрибут mana.
  • Реализовать экономику, в которой персонажи могут покупать и продавать предметы, например, зелья здоровья.
  • Усовершенствуйте боевую систему, добавив несколько типов атак, несколько врагов за бой и возможность использовать предметы, ослабляющие врагов или временно усиливающие персонажей.
  • Создайте игровой мир с различными областями, в которых игрок может путешествовать и в которых встречаются различные враги.
  • Внедрить NPC, с которыми игрок может разговаривать и получать от них задания.
  • Включить возможность сражения игрока против игрока.

Код бота Discord может быть размещен на Replit на постоянной основе, но вам потребуется использовать Always-on repl, чтобы он работал 24/7.

Вы можете найти наш repl ниже:
https://replit.com/@ritza/DiscordRPG

Оцените статью
devanswers.ru
Добавить комментарий