Table-Driven NPC Actions in Inform 7

This post will discuss the code structure of 18 Rooms to Home: Room 15. It will not discuss anything except the code structure, so there aren’t any spoilers – but if you’d prefer to avoid all discussion whatsoever, this is a good post to skip.

Also, this post will be most useful to intermediate Inform 7 users.

On to the architecture!

So far, Room 15 has taken as much time as the prior three rooms and the original project design combined. This is directly related to the trouble I had with the code. But I’ve solved the architecture problem, and I wanted to share my solution.

Room 15 involves multiple NPCs taking proactive actions.

Traditionally, I’ve built this kind of thing with a pile of if statements. But with multiple NPCs in motion, and each NPC’s actions depending on game states that other NPCs can affect, that’s wildly unwieldy. (I say this with great confidence, because I tried to build it that way first.)

Instead of building NPC actions with a pile of if statements, I’m running them off tables.

Anything that can be done in I7, probably has been done. (And in this case, I suspect it’s been done by Victor Gijsbers in Kerkerkruip, though I don’t know for sure.) But this is a new I7 approach for me, and I think it’s a good one.

Here’s the first chunk of code. It’s a little hard to follow with generic names, so I’ll use Caleb and Teo (from Two Serpents Rise.)

A person can be occupied or unoccupied.

An every turn rule while the location is (whatever the location is):
    if Caleb is not occupied:
        execute the Table of Caleb Actions;
    if Teo is not occupied:
        execute the Table of Teo Actions;
    …
    (until all the NPCs have execution rules)
    …
    now Caleb is not occupied;
    now Teo is not occupied;
    …
    (until all the NPCs have been listed)

Action executed is a truth state that varies. Action executed is false.

To execute the Table of Caleb Actions:
    now action executed is false;
    let row number be 0;
    repeat through the Table of Caleb Actions:
        let the row numbe be row number + 1;
        if action executed is false and the completed entry is false:
            if the row number is Caleb-valid:
                say “[message entry][line break]”
                if there is an activity entry:
                    carry out the activity entry activity;
            now the completed entry is true;
            now action executed is true;
            say “[conditional paragraph break]”.

(This code could be more compact – for instance, there are a few things that could be stored in variables on NPCs. But I think it’s easier to read if it’s unpacked this way.)

[Edited 7/27/15 – Also, this can be done with rules instead of activities, which is an improvement. I’m leaving this post as-is, but see the modification for rules here.]

Each table is structured like this:

Table of Caleb Actions
completed    var1        var2        var3        ...    activity    message
false        [value]        [value]        [value]        ...    [activity]    [string]
false        [value]        [value]        [value]        ...    [activity]    [string]
false        [value]        [value]        [value]        ...    [activity]    [string]
...
(until all the possible actions for Caleb have been listed)

Each of the vars above describes a specific condition that will be tested in the validity check. In Room 15, most of these columns are global truth states, and the other columns check whether an object is present in the location.

This is how the execution activity checks the validity of each row.

To decide if (position – a number) is Caleb-valid:
    choose row position in the Table of Caleb Actions;
    if there is a var1 entry:
        if (the var1 entry is [value]) and (var1 is not [value]):
            decide no;
        if (the var1 entry is not [value]) and (var1 is [value]):
            decide no;
    if there is a var2 entry:
        if (the var2 entry is [value]) and (var2 is not [value]):
            decide no;
        if (the var2 entry is not [value]) and (var2 is [value]):
            decide no;
    …
    (until there’s a check for every variable in the columns)
    …
    decide yes.

A second time, in English rather than code:

  • Every turn, the game goes through each NPC in order.
  • At each NPC, it runs down the NPC’s table in search of a valid row in the table that hasn’t been completed yet.
  • If it finds a valid row, it prints the message from that row.
  • If there’s also an activity in that row, it carries out the activity.
  • Then it marks the row completed, so it won’t do it again.

Here’s an example, filling in those vars with something more specific:

Table of Caleb Actions
 completed    has-cards    Teo-playing    high-stakes    activity    message
 false        true        false        --        --        "Caleb surveys the room as the cards shuffle themselves against his palms."

To decide if (position – a number) is Caleb-valid:
    choose row position in the Table of Caleb Actions;
    if there is a has-cards entry:
        if (the has-cards entry is true) and (Caleb does not have the playing cards):
            decide no;
        if (the has-cards entry is false) and (Caleb has the playing cards):
            decide no;
    if there is a Teo-playing entry:
        if (the Teo-playing entry is true) and (Teo is not in the location):
            decide no;
        if (the Teo-playing entry is false) and (Teo is in the location):
            decide no;
    if there is a high-stakes entry:
        if (the high-stakes entry is true) and (high-stakes is false):
            decide no;
        if (the high-stakes entry is false) and (high-stakes is true):
            decide no;

    decide yes.

This is absolutely a case where it’s better to build and alter your tables in a spreadsheet program. Storing strings will make your tables extremely wide, so the rows will wrap around and the columns will be hard to differentiate by hand.

Including a status for each variable is optional. If you leave a row entirely empty (by putting two dashes in the cell), then the NPC will execute that row as soon as it reaches it. This is useful when the NPCs aren’t all present from the start. You can have an NPC that takes several actions on its own before the other NPCs arrive.

Activities are optional, and they should not include any messages. The point of keeping the messages in the table is to ensure you can look at them all at once and resort them easily, rather than having to scroll up and down to figure out which message needs to be moved up or down in the table.

The “occupied” truth state is for shutting down an NPC from taking actions for the rest of the turn. This allows actions to be bound between multiple NPCs. For example:

Caleb deals out a hand of five cards to each player.

Teo picks up her opening hand, peeks at the cards, and lays them back down again.

This looks good – but it’s a real problem if Teo peeks at her opening cards while Caleb is still shuffling, or if Caleb bids and raises before Teo looks at her hand for the first time. Instead, I would bind these two actions together on Caleb.

Caleb deals out a hand of five cards to each player.[paragraph break]Teo picks up her opening hand, peeks at the cards, and lays them back down again.

In the activity field, I would put distract-Teo, which goes with:

Distract-Teo is an activity.
Rule for distract-Teo:
    now Teo is occupied.

This would block Teo from taking an action, so I can put this anywhere that I want Teo occupied for the rest of the turn, but it’s only useful to use on NPCs who take their actions in front of Teo. Once the turn ends, everyone’s occupied status will return to false, so if Elayne takes her action after Teo, it isn’t useful to have Elayne block Teo.

In the future, for situations with only one NPC, I’ll probably stay with stacked if statements. It just saves time on infrastructure.

But for highly volatile situations – ones where the NPCs are caroming off each other like pinballs, and each of the player’s actions whacks the flippers again – I think this is the right approach. I’ll come back to it again.


Thank you to everyone supporting Sibyl Moon through Patreon!
If this post was useful or interesting, please consider becoming a patron.

Bookmark the permalink.

4 Comments

  1. Anssi Räisänen

    Hi,
    in the coding example, what is the difference between ‘not occupied’ and ‘unoccupied’?

  2. This is incredibly interesting. I have a WIP sitting that needs to track on and off-stage (out of location) actions of about five NPCs and I’ve been putting it off because scheduling their behaviors hurts my mind to think about. My original thought was to make “task action” objects for them to carry around like instructions until they were fulfilled. I am definitely going to do some tests with your method, though!

  3. Andrew Plotkin recommended shifting from activities to rules, and he’s absolutely right. More detail at http://www.sibylmoon.com/table-driven-npc-update/.

Leave a Reply

Your email address will not be published. Required fields are marked *