LGTM Devlog 27: 분기 퀘스트

휴, 며칠이 지났습니다. 나는 더 많은 리팩토링을 수행했고 마침내 퀘스트 정의가 작동하는 방식에 만족합니다. 이 게시물의 코드는 921090e입니다.

단계 실행 루프



이제 스테이지에 실행할 미리 정의된 함수가 있으며 실행은 다음과 같습니다.
  • 퀘스트 그래프에서 갈 준비가 된 퀘스트 스테이지 가져오기
  • 이미 완료된 경우 건너뛰고 완료로 표시
  • 스테이지 인스턴스화
  • 실행 단계의 prepare() 메서드. 이는 스테이지에서 필요한 데이터를 가져오기 위해 작업을 수행해야 하는 경우를 위한 것입니다
  • .
  • 단계의 condition()를 실행하여 단계가 실행되어야 하는지 확인합니다. 이 메서드는 스테이지를 실행할 준비가 되었는지 여부를 결정해야 합니다
  • .
  • 조건이 True를 반환하면 execute() 작업을 수행하기 위해 실행할 수 있습니다
  • .
  • 마지막으로 is_done() 확인하여 퀘스트 완료 여부를 판단합니다.

  • 몇 가지 에지 조건을 추가로 처리하는 실제 코드입니다.

    while self.graph.is_active():
        ready_nodes = self.graph.get_ready()
    
        if not ready_nodes:
            log.info("No more ready nodes, stopping execution")
            break
    
        log.info("Got Ready nodes", ready_nodes=ready_nodes)
    
        for node in ready_nodes:
            # skip if completed, avoids triggering two final stages
            if self.complete:
                log.info("Done flag set, skipping the rest")
                return
    
            # completed node: TODO: just not put completed nodes into the graph?
            if node in self.completed_stages:
                self.graph.done(node)
                log.info(
                    "Node is already complete, skipping",
                    node=node,
                    complete=self.completed_stages,
                )
                continue
    
            log_node = log.bind(node=node)
            log_node.info("Begin processing stage")
    
            # instantiate stage and execute
            StageClass = self.stages[node]
            stage = StageClass(self)
            stage.prepare()
    
            if stage.condition():
                log_node.info("Condition check passed, executing")
                stage.execute()
    
                if stage.is_done():
                    log_node.info("Stage reports done")
                    self.completed_stages.append(node)
                    self.graph.done(node)
    
    log.info("Done processing node")
    


    이 루프를 실행하면 이전 노드가 완료된 후에만 사용할 수 있는 노드를 포함하여 퀘스트 트리의 모든 처리 가능한 부분을 순환합니다.
    Stage 추상 기본 클래스는 이제 다음과 같습니다.

    class Stage(ABC):
        @property
        @abstractmethod
        def children(cls) -> List[str]:
            """ List of children nodes of this stage """
            return NotImplemented
    
        def prepare(self) -> None:
            """ Any preparation for the stage """
            return
    
        def condition(self) -> bool:
            """ Function that will decide whether to execute """
            return True
    
        def execute(self) -> None:
            """ Run the stage """
            return
    
        def is_done(self) -> bool:
            """ Returns whether quest was completed """
            return True
    
        def __init__(self, quest: Quest):
            self.quest = quest
    
        def __repr__(self):
            return f"{self.__class__.__name__}(quest={repr(self.quest)})"
    


    볼 수 있듯이 children 속성만 구현하면 됩니다. 기본적으로 condition()is_done() 재정의되지 않으면 True를 반환합니다.
    condition()is_done()가 모두 있는 이유는 다음과 같은 퀘스트를 예로 들 수 있습니다.
  • 퀘스트가 플레이어에게 몇 가지 정보를 찾고 댓글에 답장하도록 요청합니다.
  • condition() 이 퀘스트 단계에서 주석을 가져와야 하는지 여부를 결정합니다(일부 알림 트리거에서?).
  • execute() 데이터를 가져오고 값을 확인한 다음 "예, 그랬습니다."또는 "아니요, 옳지 않습니다. 다시 시도하십시오"라는 응답을 보냅니다.
  • 후자의 경우 is_done()는 False를 반환하고 플레이어는 진행하지 않으며 다른 시도를 할 수 있습니다. 전자의 경우 is_done()는 True를 반환하고 플레이어는 다음 단계
  • 로 진행합니다.

    정황



    단계의 조건부 실행을 처리할 수 있는 ConditionStage라는 새로운 종류의 단계를 추가했습니다. 이것이 없으면 퀘스트를 분기할 수 있지만 분기가 실행되는지 여부는 제어할 수 없습니다. 기본 클래스의 condition() 메서드를 재정의하고 퀘스트 정의가 확인할 퀘스트 데이터 구조의 데이터를 지정할 수 있습니다.

    class ConditionStage(Stage):
        """ For conditional branch execution """
    
        @property
        @abstractmethod
        def variable(cls) -> str:
            """ Name of the variable to check """
            return NotImplemented
    
        # the variable to check against
        compare_variable: ClassVar[Optional[str]] = None
    
        # the value to check against, if compare_variable is None
        compare_value: ClassVar[Any] = None
    
        # the operator to use comparison on
        operator: ClassVar[Callable[..., bool]] = operator.eq
    
        def condition(self) -> bool:
            value_left = getattr(self.quest.quest_data, self.variable)
    
            if self.compare_variable is not None:
                value_right = getattr(self.quest.quest_data, self.compare_variable)
            else:
                value_right = self.compare_value
    
            return self.operator(value_left, value_right)
    


    클래스의 구체적인 구현은 다음과 같습니다.

    class TestQuestBranching(Quest):
        class QuestDataModel(QuestBaseModel):
            value_a: int = 1
            value_b: int = 2
    
        version = VersionInfo.parse("1.0.0")
        difficulty = Difficulty.RESERVED
        description = "This is a quest to test branching"
    
        class Start(DebugStage):
            children = ["BranchA", "BranchB"]
    
        class BranchA(ConditionStage):
            children = ["EndingA"]
            variable = "value_a"
            compare_variable = "value_b"
    
        class BranchB(ConditionStage):
            children = ["EndingB"]
            variable = "value_a"
            operator = operator.gt
            compare_value = 10
    
        class EndingA(FinalStage):
            children = []
    
        class EndingB(FinalStage):
            children = []
    


    여기에 두 개의 별도 엔딩이 있는 두 개로 분기되는 퀘스트가 있습니다. 첫 번째 분기의 조건은 value_a == value_b 이고 두 번째 분기의 조건은 value_a > 10이며, 파이썬의 내장 operator 라이브러리를 사용하여 operator.gt(a, b) 와 같은 비교 방법을 제공합니다.

    마지막 스테이지



    마지막으로 퀘스트를 완료로 표시하는 방법이 필요합니다. children가 없는 노드는 단순히 다음 단계를 트리거하지 않지만 퀘스트의 끝을 의미하지 않을 수 있습니다(막다른 지점일 수 있음). 따라서 FinalStage Stage의 구체적인 구현은 단순히 퀘스트의 complete 부울을 True로 설정합니다. (퀘스트 데이터 모델의 일부로 이것을 포함하도록 퀘스트 저장소를 리팩토링했습니다.

    class FinalStage(Stage):
        """ For ending the quest """
    
        def __init_subclass__(cls):
            cls.children = []
    
        def execute(self) -> None:
            self.quest.complete = True
    



    다양한 루프의 이러한 구체적인 구현을 통해 이제 조건을 충족하고 완료로 표시해야 하는 퀘스트를 수행할 수 있습니다.

    좋은 웹페이지 즐겨찾기