LGTM Devlog 19: Game data/quest storage
Tags: devjournal, python
Published: 2021-01-15 10:29:59

Not a lot of progress since last time, I'm still mulling over the mechanics of how data will be stored. One thing on my mind is how to handle updates - when I inevitably have to update or patch the quest data when people are already playing it.

I figured I should use [semver](https://semver.org/) as a convention, and manually update the version. At least that will tell us when there are version problems. Perhaps it could be used to notify me/the player, perhaps we can restart the quest, or maintain multiple copies of the quest as needed.

# Semver
To do this I will use the package [python-semver](https://github.com/python-semver/python-semver) to easily handle it. I'll define a function that I can run on the current save data version, and the new code data version, to say whether this upgrade is safe or not:

```py
def semver_safe(start: VersionInfo, dest: VersionInfo) -> bool:
    """ whether semver loading is going to be safe """
    if start.major != dest.major:
        return False

    # check it's not a downgrade of minor version
    if start.minor > dest.minor:
        return False
    
    return True
```

So, my rules are simply: major version numbers must match. An upgrade in minor version number is also ok (but not downgrade, but this should be rare)

Test are accordingly written:

```py
@pytest.mark.parametrize("start, dest", [
    ("1.1.1", "1.1.2"), # patch increment is ok
    ("1.1.2", "1.1.1"), # patch decrement is ok
    ("1.1.1", "1.2.1"), # minor increment is ok
    ("1.1.1", "1.2.2"), # minor+patch bump is ok
    ("1.1.2", "1.2.1"), # minor+patch bump is ok
])
def test_semver_safe(start, dest):
    """ Tests semver safe loading"""

    start = VersionInfo.parse(start)
    dest = VersionInfo.parse(dest)
    assert semver_safe(start, dest) == True

@pytest.mark.parametrize("start, dest", [
    ("2.1.1", "1.1.1"), # major increment not safe
    ("1.1.1", "2.1.1"), # major decement not safe
    ("1.2.1", "1.1.1"), # minor decrement not safe (no forward compatibility guarantee)
])
def test_semver_unsafe(start, dest):
    """ Tests semver unsafe loading"""

    start = VersionInfo.parse(start)
    dest = VersionInfo.parse(dest)
    assert semver_safe(start, dest) == False
```

# Quest data storage
I think I will have each quest as a python object, eventually it'll have each of the stages of the quest in it, which when executed allows the game to load the player's game data, find the player's current position in the quest, and trigger the next action as appropriate.

So I need each quest object (and how we select and invoke the quest object I'll figure out later) to be able to load the player's data, check the save data version against the code version using the `semver_safe` function above, and update the data. So far, it looks like this:

```py
class Quest:
    def __init__(self, version: str, quest_data: dict):
        self.semver = VersionInfo.parse(version)
        self.quest_data = quest_data

    def load(self, save_data: dict) -> None:
        """ Load save data back into structure """

        # check save version is safe before upgrading
        save_semver = VersionInfo.parse(save_data[VERSION_KEY])
        if not semver_safe(save_semver, self.semver):
            raise QuestLoadError(f"Unsafe version mismatch! {save_semver} -> {self.semver}")

        self.quest_data.update(save_data)

    def update_save_data(self) -> dict:
        """ Updates save data with new version and output """

        self.quest_data[VERSION_KEY] = self.semver
        return self.quest_data
```

With the corresponding tests:
```py
@pytest.fixture
def quest():
    return Quest("2.2.2", {"a": 1})

def test_quest_load_fail(quest):
    """ Tests a quest load fail due to semver mismatch """

    bad_version_quest_data = {
        VERSION_KEY: "1.2.2",
        "a": 2
    }
    with pytest.raises(QuestLoadError):
        quest.load(bad_version_quest_data)

def test_quest_load_save(quest):
    """ Tests a quest load fail due to semver mismatch """

    quest_data = {
        VERSION_KEY: "2.1.1",
        "a": 2
    }

    quest.load(quest_data)
    assert quest.update_save_data()["a"] == quest_data["a"]
    
```

Not very exciting, I know.
___
Next, I need to figure out how these quests are stored and triggered