Демонстрация объектно-ориентированного программирования и шаблонов проектирования с помощью диаграмм классов и кода Python с использованием классов RPG.

Введение и мотивация

Задолго до того, как я узнал о классах компьютерного программирования и о том, как они используются для объектно-ориентированного программирования (ООП), я знал о классах в RPG (ролевых играх). Ролевые игры основаны на актерской игре, и игрокам необходимо создать персонажа на основе выбора класса. Каждый класс имеет свои особенности, особые способности и навыки. Например, в игре на тему средневекового фэнтези распространенными классами являются Боец, Волшебник и Разбойник. Бойцы хорошо известны своими боевыми навыками, Волшебники — использованием магии, а разбойники — хитростью и универсальностью.

Однако персонажи с одним и тем же классом не одинаковы. Некоторые классы RPG имеют подклассы и множество возможностей внутри одного класса. Боец, например, может сосредоточиться на стрельбе из лука, боях на мечах, боях с двумя оружиями и многом другом. То же самое справедливо для большинства классов RPG. Настольные системы, такие как D&D и Pathfinder, хорошо известны тем, что разработали очень большие и иногда сложные системы для создания персонажа, основанного на различных классах, подклассах и развитии уровней.

В компьютерном программировании классы не менее важны, чем в ролевых играх. Многие языки программирования (например, Python, Java и Objective-C) являются объектно-ориентированными и используют классы в качестве основы для создания объектов. Точное определение объектно-ориентированного языка сложное и может вызвать споры, особенно когда речь идет о языке под названием JavaScript, который основан на прототипах, а не на классах. Кто-то определяет его как объектно-ориентированный, а кто-то нет.

Здесь я не собираюсь вести обсуждение того, что именно представляет собой объектно-ориентированное программирование, и не собираюсь слишком глубоко говорить о конкретных принципах, лежащих в его основе. Но если вам интересно узнать больше и узнать, почему ООП необходимо, я рекомендую эту статью, в которой рассказывается об этом, а также демонстрируется использование классов RPG. Для получения дополнительной информации о шаблонах проектирования я рекомендую этот веб-сайт, а для получения дополнительных реализаций примеров кода — этот репозиторий. Тем не менее, я покажу несколько действительно интересных примеров использования классов для создания объектов, используя идеи классов ООП, которые реализуют классы RPG, и шаблоны проектирования для связи нескольких классов в вымышленной системе. Существует множество игр и приложений с созданием персонажей на основе классов RPG, и они, вероятно, используют некоторые шаблоны дизайна в своем коде.

Эта другая статья дает введение в ООП в python и иллюстрирует разницу между классами и объектами с покемоном, объект (Пикачу) является экземпляром класса (покемон), а класс имеет атрибуты и методы:

Первый класс: Боец

Здесь я создаю класс Fighter с двумя подклассами closeFighter и rangedFighter. Чтобы создать класс, нам нужно инициализировать атрибуты, а затем определить методы. Слово self, которое стоит в начале функции, дает им доступ к другим переменным и функциям внутри класса.

Используя принцип ООП, называемый наследованием, класс Fighter имеет два подкласса (как называется в RPG) или дочерних классов (как называется в компьютерном программировании), называемых closeFighter и rangedFighter. Эти потомки имеют все атрибуты и методы родительского класса Fighter, но для каждого из них переписан метод extraAction. Это основано на принципе ООП, называемом полиморфизмом («много форм»), потому что метод extraAction имеет одно и то же имя в дочерних классах, но имеет другую форму, поскольку каждый подкласс применяет его по-своему.

На приведенной ниже диаграмме показана диаграмма классов UML. Стрелки представляют наследование, а метод extraActionn переписывается в каждом подклассе.

#creating class
class Fighter:

    #initializing attributes
    def __init__(self, name, weapon='Weapon', armor='Armor'):
        self.name = name
        self.weapon = weapon
        self.armor = armor

   #defining methods
    def attack(self):
        print(self.name+' attacks using '+self.weapon)
    def defend(self):
        print(self.name+' defends with '+self.armor)
    def extraAction(self):
        print('Do something')

#example of object created using Fighter class
geralt = Fighter('Geralt')
geralt.attack()
geralt.defend()
geralt.extraAction()
Geralt attacks using Weapon
Geralt defends with Armor
Do something
#creating subclass of Fighter - inheritance

class closeFighter(Fighter):
    def __init__(self, name):
        Fighter.__init__(self,name=name, weapon='GreatSword', armor='HeavyArmor')

    def extraAction(self):
        print(self.name+' kicks the enemy')

class rangedFighter(Fighter):
    def __init__(self, name):
        Fighter.__init__(self, name=name, weapon='Bow', armor='MediumArmor')

    def extraAction(self):
        print(self.name+' gets further away from the enemy')

close = closeFighter('Close Fighter')
close.attack()
close.defend()
close.extraAction()

print()

ranged = rangedFighter('Ranged Fighter')
ranged.attack()
ranged.defend()
ranged.extraAction()
Close Fighter attacks using GreatSword
Close Fighter defends with HeavyArmor
Close Fighter kicks the enemy

Ranged Fighter attacks using Bow
Ranged Fighter defends with MediumArmor
Ranged Fighter gets further away from the enemy

Прототип с классом Fighter

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

В настольной ролевой игре один человек обычно является DM (мастером подземелий). Этот человек отвечает за создание задач и руководство другими игроками в игре. DM обычно управляет NPC (неигровыми персонажами), и у DM обычно есть прототипы персонажей, которых они могут использовать, вместо того, чтобы каждый раз создавать NPC. В видеоигре RPG действует та же концепция NPC, но обычно за ней стоит AI (искусственный интеллект), а не DM. В обоих случаях несколько NPC совершенно одинаковы, поэтому интересно использовать прототипы, а не создавать каждый раз новый класс.

# NPC prototype
from copy import deepcopy

class NPCs():
    def __init__(self):
    
        #dictionary used for prototyping
        self.itens = {
            'closeFighter_NPC': closeFighter('closeFighter_NPC'),
            'rangedFighter_NPC': rangedFighter('rangedFighter_NPC')
        }

    #creating NPC - prototyping instead of creating a new class every time
    def create(self, item):
        return deepcopy(self.itens[item])
# prototype example - multiple fighters


npcs = NPCs()

close1 = npcs.create('closeFighter_NPC')
close2 = npcs.create('closeFighter_NPC')
close3 = npcs.create('closeFighter_NPC')
ranged1 = npcs.create('rangedFighter_NPC')
ranged2 = npcs.create('rangedFighter_NPC')
ranged3 = npcs.create('rangedFighter_NPC')

close1.attack()
close2.attack()
close3.attack()
ranged1.attack()
ranged2.attack()
ranged3.attack()
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
rangedFighter_NPC attacks using Bow
rangedFighter_NPC attacks using Bow
rangedFighter_NPC attacks using Bow
# prototype example - creating an army of 30 fighters of close range
army = []
for _ in range(30):
    army.append(npcs.create('closeFighter_NPC'))

#making every soldier attack
for soldier in army:
    soldier.attack()
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword
closeFighter_NPC attacks using GreatSword

Стратегия для колдовских классов

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

У персонажей RPG есть способности, которые описывают, как они обладают определенной характеристикой, и их способности делают их более вероятными для выполнения определенных действий, чем другие. Например, человек с высокой харизмой обладает сильным характером и может легко влиять на других. С другой стороны, такие способности, как Мудрость и Интеллект, могут показаться похожими, но Мудрость измеряет силу воли, инстинкты и интуицию, а Интеллект измеряет рассуждения и память.

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

Таким образом, шаблон проектирования стратегии очень интересен для этого случая. У нас есть, как видно на изображении ниже, схема для этой системы. Классы Wizzard, Sorcerer и Druid наследуются от класса SpellCaster. Класс SpellCaster объединяет классы ArcaneFocus и Casting. Класс ArcaneFocus имеет два дочерних класса, hasArcaneFocus и noArcaneFocus, а класс Casting имеет три дочерних класса в соответствии с типами способностей, используемых для применения: Casting_Intelligence, Casting_Charisma и Casting_Wisdom.

## arcane focus


class ArcaneFocus:
  
    def focus(self):
        raise NotImplemented

class hasArcaneFocus(ArcaneFocus):
    
    def focus(self):
        print("This character can use it's arcane focus to cast a spell, no need for material spell components.")

class noArcaneFocus(ArcaneFocus):

    def focus(self):
        print("This characater can't use an arcane focus to cast a spell, material spell components are needed.")


## casting


class Casting:
  
    def spellcasting(self):
        raise NotImplemented

class Casting_Intelligence(Casting):

    def spellcasting(self):
        print("This character's spellcasting ability comes from careful study "\
                "of books and memorization of rituals to cast a spell, therefore their spellcasting ability is Intelligence.")
    
class Casting_Charisma(Casting):

    def spellcasting(self):
        print("This carachter's spellcasting ability comes from the force of their personality and willpower, "\
                "therefore their spellcasting ability is Charisma.")
    
class Casting_Wisdom(Casting):

    def spellcasting(self):
        print("This character's spellcasting ability comes from their innate knowledge and connection with a diety or "\
                "force of nature, therefore their spellcasting ability is Wisdom.")


## creating classes using strategy

class SpellCaster:

    arcaneFocus = None
    casting = None

    def __repr__(self):
        raise NotImplementedError

    def spells(self):
        print("This character is a spellcaster.")

    def arcane_focus(self):
        self.arcaneFocus.focus()

    def spellcasting(self):
        self.casting.spellcasting()


class Wizzard(SpellCaster):

    arcaneFocus = hasArcaneFocus()
    casting = Casting_Intelligence()

    def __repr__(self):
        return "This character is a Wizzard."

class Sorcerer(SpellCaster):

    arcaneFocus = hasArcaneFocus()
    casting = Casting_Charisma()

    def __repr__(self):
        return "This character is a Sorcerer."

class Druid(SpellCaster):

    arcaneFocus = noArcaneFocus()
    casting = Casting_Wisdom()

    def __repr__(self):
        return "This character is a Druid."



## test case



for spellcaster in (Wizzard(), Sorcerer(), Druid()):
    print(spellcaster)
    spellcaster.spells()
    spellcaster.arcane_focus()
    spellcaster.spellcasting()
    print("")
This character is a Wizzard.
This character is a spellcaster.
This character can use it's arcane focus to cast a spell, no need for material spell components.
This character's spellcasting ability comes from careful study of books and memorization of rituals to cast a spell, therefore their spellcasting ability is Intelligence.

This character is a Sorcerer.
This character is a spellcaster.
This character can use it's arcane focus to cast a spell, no need for material spell components.
This carachter's spellcasting ability comes from the force of their personality and willpower, therefore their spellcasting ability is Charisma.

This character is a Druid.
This character is a spellcaster.
This characater can't use an arcane focus to cast a spell, material spell components are needed.
This character's spellcasting ability comes from their innate knowledge and connection with a diety or force of nature, therefore their spellcasting ability is Wisdom.

Государственный шаблон дизайна

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

from abc import ABCMeta, abstractmethod

class CharacterState:

    @abstractmethod
    def move(self):
        pass
  
    @abstractmethod
    def investigate(self):
        pass
  
    @abstractmethod
    def attack(self):
        pass

class CharacterStateConscious(CharacterState):
    def move(self):
        print('Can move normally.')
    def investigate(self):
        print('Can investigate normally.')
    def attack(self):
        print('Can attack normally.')

class CharacterStateUnconscious(CharacterState):
    def move(self):
        print("Can't move.")
    def investigate(self):
        print('Can investigate their dreams.')
    def attack(self):
        print("Can't attack.")

class CharacterStateFrightened(CharacterState):
    def move(self):
        print("Can't move closer to the source of fear.")
    def investigate(self):
        print('Has disadvantage investigating while seeing their source of fear.')
    def attack(self):
        print('Has disadvantage attacking while seeing their source of fear.')

class CharacterStateBlinded(CharacterState):
    def move(self):
        print('Can move guided by their senses other than sight.')
    def investigate(self):
        print('Can investigate guided by their senses other than sight.')
    def attack(self):
        print('Has disadvantage attacking.')

class Character:

    _state = None

    def __init__(self, state: CharacterState):
        self.transition_to(state)

    def transition_to(self, state: CharacterState):
        self._state = state
        self._state.context = self

    def move(self):
        self._state.move()

    def investigate(self):
        self._state.investigate()

    def attack(self):
        self._state.attack()



## test case

#starting character as conscious
character = Character(CharacterStateConscious())

#conscious state
print('\033[1m'+'Conscious state'+'\033[0m')
character.move()
character.investigate()
character.attack()
print()

#unconscious state
print('\033[1m'+'Unconscious state'+'\033[0m')
character.transition_to(CharacterStateUnconscious())
character.move()
character.investigate()
character.attack()
print()

#frightened state
print('\033[1m'+'Frightened state'+'\033[0m')
character.transition_to(CharacterStateFrightened())
character.move()
character.investigate()
character.attack()
print()

#blinded state
print('\033[1m'+'Blinded state'+'\033[0m')
character.transition_to(CharacterStateBlinded())
character.move()
character.investigate()
character.attack()
Conscious state
Can move normally.
Can investigate normally.
Can attack normally.

Unconscious state
Can't move.
Can investigate their dreams.
Can't attack.

Frightened state
Can't move closer to the source of fear.
Has disadvantage investigating while seeing their source of fear.
Has disadvantage attacking while seeing their source of fear.

Blinded state
Can move guided by their senses other than sight.
Can investigate guided by their senses other than sight.
Has disadvantage attacking.