The Imitable Process of Ryan Veeder: Advanced Autosaving in Inform 7

In a previous post I explicated the basic principles of how to automatically track and restore the player’s progress in an Inform 7 game. What it really comes down to is this:

  • Represent the parameters of your game’s progression in a table.
  • Whenever the player does something worth autosaving, record it in that table, and then write that table to an external file.
  • When play begins, read that external file and use those data to reconstruct the player’s progress.

I said we’d get into some ways of representing progress in a nonlinear game. The first thing that comes to mind is my game The Lurking Horror II: The Lurkening, in which the player character learns various spells that let you manipulate the environment and solve puzzles. Learning certain spells requires you to learn certain other spells first, but the “tech tree” has enough branches that we can’t predict in what order the player will discover everything.

The game is also based on a time loop, where you only have nine turns to try to make any headway before the world ends and you have to start the game over. This game is a great candidate for an autosave mechanic, because the only form of concrete progress is learning spells, and we want that progress to be preserved when the game starts over. Maybe I should go ahead and update the game with an autosave mechanic…

OH WAIT I’M BUSY WITH SOMETHING ELSE.

Well, here’s how I’ll do it, when I get around to it. (To avoid spoiling the game for you, I’ll come up with some new names for spells.) First of all, even if I weren’t trying to autosave anything, I might represent the player’s spell-acquisition with a table like this. In the “learned” column, I’m using 0 to represent an unlearned spell and 1 to represent a learned spell.

Table of Spells
spell learned
Ambradadra 0
Bijusmolt 0
Chasminsuni 0
Dielectric 0

We can use this data structure to facilitate the learning and casting of these spells like this:

Before spellcasting behavior:
choose row with spell of the current action in Table of Spells;
if learned entry is 0:
say "You don't know that spell." instead.

[We'll have to point out that each spellcasting action qualifies as "spellcasting behavior" for the general rule above to work.]

Ambradadra is an action applying to nothing.
Ambradadra is spellcasting behavior.
Understand "Ambradadra" and "cast Ambradabra" as Ambradadra.

Instead of Ambradadra:
say "You cast Ambradadra!" [I don't know what it does. I just made it up!]

The player carries the Scroll of Dadra.

Instead of examining the Scroll of Dadra:
say "You read and understand the spell called Ambradadra.";
choose row with spell of Ambradadra in Table of Spells;
now learned entry is 1.

With similar setups for all the other spells. Now, to start autosaving the player’s spell-learning progress, we’d like to do something like this:

The File of Spells is called "spellfile".

Instead of examining the Scroll of Dadra:
say "You read and understand the spell called Ambradadra.";
choose row with spell of Ambradadra in Table of Spells;
now learned entry is 1;
write File of Spells from Table of Spells.

When play begins:
if File of Spells exists:
read File of Spells into Table of Spells.

But this doesn’t work—It compiles, but it ends up generating a bunch of runtime errors. Remember that Inform 7 can only handle writing “safe” types of data from tables into files. The names of actions aren’t “safe” for these purposes, because they’re tied to memory locations that can change whenever the game is compiled. When the game tries to find the “row with spell of Ambradadra in Table of Spells,” it can’t, because that memory address belongs to some other action now!

We can get around this, though. What we need to do is represent each “unsafe” data point with a specific “safe” data point, like a number. We’ll translate between data types by using two tables.

Table of Spell Correspondences
spell code
Ambradadra 101
Bijusmolt 102
Chasminsuni 103
Dielectric 104

Table of Spells
code learned
101 0
102 0
103 0
104 0

These “codes” that we use to refer to actions can be whatever number we want. We just don’t want two spells to share the same code. I’m only formatting them as 101, 102… because it makes things a bit easier for me to understand at a glance.

Now we have to change the rules for checking whether you know a spell and teaching you a spell: They have to carry the information for each action from “spell” to “code” to “learned” and back.

Before spellcasting behavior:
choose row with spell of the current action in Table of Spell Correspondences;
let current code be code entry;
choose row with code of the current code in Table of Spells;
if learned entry is 0:
say "You don't know that spell." instead.

Instead of examining the Scroll of Dadra:
say "You read and understand the spell called Ambradadra.";
choose row with spell of Ambradadra in Table of Spell Correspondences;
let current code be code entry;
choose row with code of current code in Table of Spells;
now learned entry is 1;
write the File of Spells from the Table of Spells;

When play begins:
if File of Spells exists:
read the File of Spells into the Table of Spells;

That final “when play begins” rule hasn’t changed, but it points out something important: Only the Table of Spells, the one that contains “safe” data, exchanges information with the external File of Spells. The Table of Spell Correspondences only exists within the game file.

That means we have to be careful! If we change any of the correspondences we use to connect internal data to external data, our save files won’t work properly! If you’re paying attention to the format of your tables, though, you’re unlikely to make any silly mistakes—like changing the spell code of Dielectric to 109 in one table but not the other. (This “danger” is also kind of a feature in disguise: As long as we don’t change any of the correspondences, we can add new stuff to our game, and save files from previous versions will still work! Holy cow!)

We can expand this model to record different types of progress. Imagine a game where you have to feed a bunch of cute fuzzy dinosaurs:

Table of Dinosaur Correspondences
dinosaur code
Stegosaurus 101
Triceratops 102
Iguanadon 103

Table of Dinosaurs
code sated
101 0
102 0
103 0

A dinosaur is a kind of animal.
The stegosaurus is a dinosaur.
The triceratops is a dinosaur.
The iguanadon is a dinosaur.

A dinosaur can be hungry. A dinosaur is usually hungry.

When play begins:
if File of Dinosaurs Exists:
read File of Dinosaurs into Table of Dinosaurs;
repeat through Table of Dinosaurs:
let current code be code entry;
if sated entry is 1:
choose row with code of current code in Table of Dinosaur Correspondences;
now dinosaur entry is not hungry.

And the final entry doesn’t have to be 0 or 1: it can be a variable expressing how happy a dinosaur is, or a code number corresponding to which funny hat a dinosaur should be wearing. If you want to store multiple types of data about each dinosaur, your Table of Dinosaurs can have more columns.

This technique of writing out two connected tables is most useful with small games. When I started programming The Lurkening, I had already charted out in great detail what all the rooms and puzzles and spells would be. But in a big game like Ryan Veeder’s Authentic Fly Fishing, your plans might not be so precise (and they’re more likely to change as you go along).

Rather than revise both of our tables every time we add something saveable to our game, we can give Inform 7 the information it needs to construct those tables for us! Here I present a highly simplified (but still very serviceable) version of the method I use to autosave your inventory in Ryan Veeder’s Authentic Fly Fishing. First, we’ll establish a data structure.

"A Game About Groceries"

Section 1 - Setting Things Up

The File of Groceries is called "groceries".

Table of Groceries
code (a number)
with 200 blank rows

Table of Grocery Correspondences
item (an object) code (a number)
with 200 blank rows

A thing has a number called UPC.

A thing can be grabbed or ungrabbed. A thing is usually ungrabbed.

“200 blank rows” is probably way more than enough—Ryan Veeder’s Authentic Fly Fishing, a fairly large game, only has about 60 portable objects in it… I think.

The number we’re assigning to each item is called the “UPC” partly because it amuses me to use retail terminology in a grocery store game, but it’s also very appropriate to the function of the property: It’s a number that identifies a specific item.

“Grabbed” and “ungrabbed” really mean “having been picked up by the player” and “not having been picked up by the player.” This will end up being the same as “has an entry on the Table of Groceries” or “doesn’t have an entry on the Table of Groceries,” but we’ll find the redundancy useful for certain syntaxes.

Next we’ll populate the game world.

Section 2 - The Supermarket

Supermarket is a room.

The orange is here. The UPC of the orange is 1.

The grapefruit is here. The UPC of the grapefruit is 2.

The lime is here. The UPC of the lime is 3.

The jar of almonds is here. The UPC of the jar of almonds is 8.

It’s not necessary to assign these UPCs (or whatever you decide to call them) in numerical order, but you do want to make sure you don’t assign the same UPC to two different items! You also want to make sure you assign a UPC to everything that needs one, obviously.

Here’s a hot tip: When you say “A thing has a number called UPC” and then you declare the existence of a thing without declaring what its UPC is, the value of that property defaults to zero. You can use this fact to exempt certain things from your autosaving—maybe your game has delicate snowflakes that quickly melt in the player character’s warm hands and so shouldn’t persist between playthroughs, or something like that.

Let’s incorporate that hot tip into our rule that autosaves the collection of inventory items:

After taking something ungrabbed:
if UPC of the noun is not 0:
now the noun is grabbed;
choose a blank row in Table of Groceries;
now code entry is UPC of the noun;
Write the file of Groceries from the Table of Groceries;
say "Taken."

Note how this only applies to “ungrabbed” things, and makes them become “grabbed,” so it never applies to the same thing twice. And at the same time we make things grabbed, we add them to the Table of Groceries (and write to the File of Groceries).

If we didn’t have the “ungrabbed” feature to refer to, we’d have to check the Table of Groceries for each item in a roundabout manner, something like:

After taking something:
say "Taken.";
let current code be the UPC of the noun;
let grabbed yet be 0;
repeat through Table of Groceries:
if code entry is current code:
now grabbed yet is 1;
if grabbed yet is 0:
choose a blank row in Table of Groceries;
now code entry is UPC of the noun;
write the file of Groceries from the Table of Groceries.

You see how this rule has to read through the whole Table of Groceries every time the player picks something up? Ridiculous!

Anyway, now that we have a File of Groceries that’s accumulating all our inventory data, it’s time to make that data get restored when we start the game over. We also have to make the game construct those tables like I said we were gonna do!

When things get this complicated, I tend to split up a single “When play begins…” rule into multiple rules, so I can see the order of operations in front of me rather than trying to hold it all in my head.

When play begins:
construct the Table of Grocery Correspondences;
construct the File of Groceries;
reconstruct the player's inventory.

To construct the Table of Grocery Correspondences:
repeat with item running through things:
if UPC of item is not 0:
choose a blank row in Table of Grocery Correspondences;
now item entry is item;
now code entry is UPC of item.

To construct the File of Groceries:
if File of Groceries exists:
read the File of Groceries into the Table of Groceries.

To reconstruct the player's inventory:
repeat through Table of Groceries:
let current code be code entry;
choose row with code of current code in Table of Grocery Correspondences;
now item entry is grabbed;
now player carries item entry.

Look carefully at the “to construct the Table of Grocery Correspondences” rule. For every save-worthy item in the game (every item that we declared a unique UPC for), the rule makes a new row in the table with that item and its UPC.

Constructing the Table of Groceries is fairly straightforward; it’s just the File of Groceries, which is just a list of the foodstuffs the player has acquired. Reconstructing the player’s inventory is pretty simple too: We just find all the items that were listed (by UPC) in the File of Groceries, and stock the player’s inventory with them. (We also mark them as “grabbed” so they don’t get added to the File of Groceries a second time.)

We can remove things from the saved inventory list if we find that desirable. Maybe if the player drops something:

After dropping something:
say "Dropped.";
if the UPC of the noun is not 0:
now the noun is not grabbed;
let current code be the UPC of the noun;
repeat through Table of Groceries:
if code entry is current code:
blank out the whole row;
write the File of Groceries from the Table of Groceries.

(“Repeat through Table of Groceries” is more tedious, but a little safer, than “Choose row with code of current code in the Table of Groceries.” If the latter fails to find a matching row, we’ll get a runtime error.)

If we want to erase all the save data, to let the player restart this game (where there’s nothing to do but pick things up and put them back down again) with a clean slate, we can write an appropriate version of the RESTART COMPLETELY action.

Completely restarting is an action out of world. Understand "restart completely" as completely restarting.

Carry out completely restarting:
say "Do you want me to erase all your progress?[paragraph break]>";
if player consents:
say "Are you sure?[paragraph break]>";
if player consents:
repeat through Table of Groceries:
blank out the whole row;
write the File of Groceries from the Table of Groceries;
end the story saying "Done! Choose RESTART to start again.";
otherwise:
say "All right, then I won't.";
otherwise:
say "All right, then I won't.";

Now we have a pretty full-featured inventory autosaving system! There’s plenty of room to make things more sophisticated: In Ryan Veeder’s Authentic Fly Fishing, the inventory restoration machinery doesn’t just drop all your stuff into your inventory, but arranges your belongings in your cabin, with certain things stored on the desk or in the cupboard according to your preference. I think that’s outside the scope of this post, though.

There are a few more autosaving tricks I’d like to show you… in another post!