LGTM Devlog 34: Characters Posting on GitHub Issues
Tags: python, gamedev, devjournal, github
Published: 2021-02-05 10:37:13

Some big changes today as multiple things are coming together! Most of the work is now in the `stages` module, the code for this pots is at commit [0e67f0d](https://github.com/meseta/lgtm/tree/0e67f0d6923024874cf048738d96061012c356eb)

# Delay stages
I've decided to instead of using a task queue system for timed events, simply add a `DelayStage` quest stage that enacts a time delay. This reduces the complexity by knocking out a piece of infrastructure, and was surprisingly easy to write given the existing infrastructure:

```py
class DelayStage(Stage):
    """ For enacting a time delay """

    @property
    @abstractmethod
    def delay(cls) -> timedelta:
        """ how much time to delay """
        return NotImplemented

    def prepare(self) -> None:
        """ On first run, insert datetime """
        if self.get_stage_data() is None:
            now = datetime.now().timestamp()
            logger.info(f"Delay stage prepare", now=now)
            self.set_stage_data(now)

    def condition(self) -> bool:
        """ Calculate whether that has elapsed """
        target = datetime.utcfromtimestamp(self.get_stage_data(0)) + self.delay
        now = datetime.now()
        logger.info(f"Delay stage prepare", now=now, target=target)
        return now > target
```

The DelayStage uses the `prepare()` event to write into its stage storage (a new free-form dict value that I've added to the quest data storage model to store stage-specific data) the target timestamp, and when the condition runs next, it tests if this time has been reached or exceeded! Very simple.

# Creating Issues
Now that we have the code for characters to post github messages, I can add a `CreateIssueStage()` that will create a new issue in a user's fork, it looks like this:

```py

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

    @property
    @abstractmethod
    def character(cls) -> Character:
        """ Which character will post the issue """
        return NotImplemented

    @property
    @abstractmethod
    def issue_title(cls) -> str:
        """ Issue title """
        return NotImplemented

    @property
    @abstractmethod
    def issue_body(cls) -> str:
        """ Issue main body """
        return NotImplemented

    # variable to store resulting issue ID in for later
    issue_id_variable: Optional[int] = None

    def execute(self) -> None:
        """ Post the issue to the fork """
        game = self.quest.quest_page.game
        game.load()
        fork_url = game.data.fork_url

        logger.info("Creating issue", title=self.issue_title, fork_url=fork_url)
        issue_id = self.character.issue_create(
            fork_url, self.issue_title, self.issue_body
        )

        if self.issue_id_variable:
            logger.info(
                "Storing issue Id in variable",
                issue_id=issue_id,
                variable=self.issue_id_variable,
            )
            setattr(self.quest.quest_data, self.issue_id_variable, issue_id)
```

It has some abstract properties for the character, issue title, and so on; and the business part of the class simply calls `character.isseu_create()` that's part of the package defined last time. The rest of the `execute()` event deals with fetching the fork_url, and saving the results in a location of your choosing, so that a stage can output the issue ID created so that other stages can use it.

In usage, it looks like this:

```py
class IntroQuest(Quest):
    class QuestDataModel(QuestBaseModel):
        issue_id: Optional[str] = None
        last_comment: Optional[datetime] = None

    version = VersionInfo.parse("0.1.0")
    difficulty = Difficulty.BEGINNER
    description = "The intro quest"

    class Start(CreateIssueStage):
        children = ["CheckNumber1"]
        character = character_garry
        issue_id_variable = "issue_id"
        issue_title = "Welcome, can you give me a hand?"
        issue_body = """\
            Hello therre!
        """
```

The reason for the `"""\` is due to the use of pythons [dedent](https://docs.python.org/3/library/textwrap.html#textwrap.dedent) method which keeps intedentation nicer in the source code for multi-line strings.

When this `Start` event fires, it will use the character defined by `character_garry` to send an issue to the player's fork, with the given issue title and body. It will also store the issue_id in the quest data model's `issue_id` key, so that next stages can re-use it.

# Testing
In order to test, to avoid spamming data somewhere public, I've over-written the test files to fake a fork to a _private_ github repo instead. This usually isn't possible on GitHub during a normal flow, but our system doesn't make a distinction, so happily accepts it as part of a test.

![Test issues](https://dev-to-uploads.s3.amazonaws.com/i/f4ops5f08vesk1l932s1.png)

I can run the tests already in the `github_webhook_listener` to test the creation of this first issue since it already starts by deleting the test user's game and starting a new one. I can also test follow-on stages using the tests already defined in `test_main_tick`