Рубрики
Без рубрики

Lgtm Devlog 26: Python Graphlib Dags для квестов

Некоторая дальнейшая очистка была сделана, значения не были выключены для объектов Sentinel, а Depen … Tag с Python, Devjournal, Gamedev.

LGTM (40 частей серии)

Некоторая дальнейшая очистка была сделана, значения не были выключены для Sentinel Objects и инъекция зависимости, начатая в прошлый раз, была расширена, чтобы также справиться с возвратными значениями, что делает Главный Функция больше отделки, и квесты и тесты были скорректированы, чтобы работать лучше (ранее было неудачное неправильное использование классовых переменных). Код можно найти в Commit 1718538

Вот несколько основных моментов:

Я определяю объект Quest Stage, чья задача состоит в том, чтобы содержать детали, которые необходимо в нашей игре, чтобы взаимодействовать с игроком. Это начинается с нашей базы Стадия Азбука На данный момент это просто содержит это:

class Stage(ABC):
    @property
    @abstractmethod
    def children(cls) -> List[str]:
        """ List of children nodes of this stage """
        return NotImplemented

    quest: Union[Quest, NoQuestType] = NoQuest

    def __repr__(self):
        return f"{self.__class__.__name__}(quest={repr(self.quest)})"

A repr , Дети Свойство, какое подклассы должны заполнить (указывая на следующие квесты на графике) и квест Свойство экземпляра, которое будет заполнено квестом, который охватывает этап, который предоставляет эта ссылка на родительский класс, чтобы получить доступ к таким вещам, как данные квеста, или объект пользователя.

Из этого Стадия BaseClass, я буду реализовать подклассы для каждого из типов этапа квеста. Прямо сейчас у меня есть только этап отладки (что ничего не делает, это просто для тестирования), и определение (но не реализация) CreateIssUestage который позже будет содержать реализацию для запуска сообщения для выпусков GitHub

class DebugStage(Stage):
    """ For debugging purposes """


class CreateIssueStage(Stage):
    """ This stage posts a new issue to a user's fork """

Будут будущие подклассы, которые будут делать такие вещи, как условия проверки (для разрешения ветвления), проверки ответов и так далее. Я выясню, как сделать это позже.

Поэтому квест с этапами может быть определена так:

class DebugQuest(Quest):
    class QuestDataModel(QuestBaseModel):
        a: int = 1

    version = VersionInfo.parse("1.0.0")
    difficulty = Difficulty.RESERVED
    description = "This is a quest to facilitate testing/debugging"

    class Start(DebugStage):
        children = ["First"]

    class First(DebugStage):
        children = ["Second"]

    class Second(DebugStage):
        children = []

Каждая стадия квеста подкласс на одном из существующих этапов и обеспечивает метаданные и другие значения, необходимые для этого класса (Они не экземпляры)

Во время подкласса следующий код работает внутри Квест объект:

class Quest(ABC):
    ... 
    @property
    @abstractmethod
    def stages(cls) -> Dict[str, Type[Stage]]:
        """ The initial default data to start quests with """
        return NotImplemented

    def __init_subclass__(cls):
        """ Subclasses instantiate by copying default data """
        # build class list
        cls.stages = {}
        for name, class_var in vars(cls).items():
            if isclass(class_var) and issubclass(class_var, Stage):
                cls.stages[name] = class_var

Это init Метод подкласса вызывается, когда Квест подкласс! И когда это произойдет, код будет собирать все свойства класса, найдут те, которые являются подклассами Стадия и вставьте их в переменную класса стадии , так что этот словарь теперь содержит имя: классная пара всех этапов. Примечание: это все еще не происходит во время экземпляра, поэтому На данный момент это все еще только занятия, с которыми мы имеем дело.

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

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

Ранее я немного говорил о Dags/Digraphs в моем Пришествие кода 2020 День 10 Пост , где я использовал универсальный Python Networkx Пакет, чтобы очень легко вычислить решение проблемы.

Однако на этот раз я использую новый Python graphlib который новый в Python 3.9!

Какой графлиб и это Topologicalsorter () позволяет нам делать две очень важные вещи:

  1. Это позволяет нам обнаружить запрещенные циклы и петли в квесте. Если кто -то случайно вернет петлю Quest Stage обратно на себя, то Graphlib будет жаловаться (и тесты потерпят неудачу)
  2. Это позволяет нам легко найти следующую доступную стадию квеста, поэтому нам не нужно делать какую -либо ручной график, чтобы найти этапы, которые готовы к запуску.

Код выглядит так, и живет в Quest ABC:

class Quest(ABC):
    ...

    def load_stages(self) -> None:
        """ loads the stages """

        # load graph
        self.graph = TopologicalSorter()
        for stage_name, StageClass in self.stages.items():
            for child_name in cast(List[str], StageClass.children):
                if child_name not in self.stages:
                    raise QuestDefinitionError(
                        f"Quest {self.__class.__name__} does not have stage named '{child_name}'"
                    )
                self.graph.add(child_name, stage_name)

        try:
            self.graph.prepare()
        except CycleError as err:
            raise QuestDefinitionError(
                f"Quest {self.__class__.__name__} prepare failed! {err}"
            ) from err

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

После того, как топологический состав загружается, работает self.graph.prepare () будет выполнять чеки на циклы и поднять исключения, если они существуют.

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

Как правило, все эти новые функции имеют полный набор тестов, включая создание плохих квестов, например, в файлах тестов определяется пара плохих квестов:

class BadStageCycle(Quest):
    version = VersionInfo.parse("1.0.0")
    difficulty = Difficulty.RESERVED
    description = "Bad quest for testing, it has malformed stages"

    class Start(DebugStage):
        children = ["Loop"]

    class Loop(DebugStage):
        """ This should form a cycle, and get flagged by test """

        children = ["Start"]


class BadStageNotExist(Quest):
    version = VersionInfo.parse("1.0.0")
    difficulty = Difficulty.RESERVED
    description = "Bad quest for testing, it has malformed stages"

    class Start(DebugStage):
        """ This references a stage that doesn't exist, and get flagged """

        children = ["Loop"]

def test_fail_stage():
    """ Test bad quests that fail due to stage problems """

    quest = BadStageCycle()
    with pytest.raises(QuestDefinitionError):
        quest.load_stages()

    quest = BadStageNotExist()
    with pytest.raises(QuestDefinitionError):
        quest.load_stages()

И несколько других тестов, которые гарантируют, что у нас есть хороший тестовый охват.

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

LGTM (40 частей серии)

Оригинал: “https://dev.to/meseta/lgtm-devlog-26-python-graphlib-dags-for-quest-stages-2h3m”