StupidRPG: Command Interrupts

I’ll talk more about the language parser in SRPG in a future post, but I need to give a little context for this discussion. In the majority of cases, SRPG is a turn-based, player-driven game. The player enters a command, the game engine parses the command, and the game state is updated accordingly. During normal execution, the player’s input is matched to a ‘verb’ (an action handler) along with relevant nouns (objects/entities) and modifiers (cardinal directions, for example). Various command patterns are supported, from the basic GO NORTH (VERB MODIFIER) to the more complex PUT BEER BOTTLE ON PEDESTAL (VERB NOUN MODIFIER NOUN).

This gives a lot of flexibility, but it’s missing support for a couple of use cases: 1) easter eggs; and 2) sass. As it turns out, the command interrupt feature has some less frivolous applications as well.

What is a Command Interrupt?

A command interrupt is a special callback that interrupts the regular flow of command processing. This allows for contextual, off-the-cuff responses from the player, along with aforementioned non-frivolous uses.

Example:


GM: Deep in the gloom, you see the fearsome troglodyte. You’re not sure how to describe it because you don’t remember what a troglodyte is.

Input: yes i do

GM: Oh, my mistake. It looks exactly like you expected a troglodyte to look.


Now let’s break down what’s happening here.

How does a Command Interrupt work?

The game does not have a verb for ‘yes’ or ‘yes i do’. This is a special case, only available for the next action after the GM’s initial text. The code looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Describe troglodyte
queueOutput("{{gm}}<p>Deep in the gloom, you see the fearsome {{tag 'troglodyte' classes='object enemy' command='x troglodyte'}}. You're not sure how to describe it because you don't remember what a troglodyte is.</p>");

NLP.interrupt(
function() { /* Init */ },
function(string){
NLP.interrupt(null);

if(string == "yes i do") {
queueOutput("{{gm}}<p>Oh, my mistake. It looks exactly like you expected a troglodyte to look.</p>");
} else {
// Resume normal input
queueOutput(parse( NLP.parse(string), {} ));
}
}
);

On line 1, the GM response is queued as normal.

The fun stuff happens on line 4, where an interrupt is registered with the NLP (the Natural Language Processor). This tells the NLP that when the player enters their next command, control should first be handed to the interrupt. The interrupt acts autonomously, deciding whether it wants to handle the action or not.

Line 5 is an optional initialization function triggered when the interrupt is queued. Without getting too far into the weeds, I’ll just say that multiple interrupts can be ‘stacked’. When one is completed (unregistered) the next is enabled.

On line 7, the interrupt deregisters itself. If the player doesn’t immediately enter the response we’re looking for, we’re not going to give them another chance. Interrupts are given first priority until they deregister themselves (this is important because it lets us do more interesting things than simple one-off responses to snarky player text).

On line 9, we check for the string we’re expecting. In this case, we’re looking for the player entering “yes i do” in response to the GM’s “you’re not sure how to describe it because you don’t remember what a troglodyte is”. If we matched the string, we queue an equally snarky response from the GM.

If the string is not matched, we re-parse the command. Since we already de-registered the interrupt, the player’s input will be handled normally, and they will have no idea that there was a special response available.

Alternate Uses

Command interrupts were initially intended for use in easter eggs: special responses from the GM based on the player inputting non-standard commands in specific situations. As it turns out, however, there are better uses for the feature. I’ll cover three use cases:

  • Raw text input
  • Menu selections
  • Dialogue trees

Raw Text Input

This is actually the first thing the player encounters when starting the game. The player is prompted for their name, and a command interrupt is used to capture the (arbitrary) text they entered. As usual, the interrupt can decide how to handle the player’s input, and whether to deregister itself or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ADD PLAYER NAME TO INTERRUPT QUEUE
NLP.interrupt(
function(){
// Leadup to player name input
queueOutput("{{gm}}<p>You are an adventurer, but not a very good one. You are known as...</p>", 2000);
queueOutput("{{gm}}<p>Hmm.</p>", 1500);
queueOutput("{{gm}}<p>Who are you again?</p>", 1000);
},
function(string){
if(string.length > 0){
player.name = string;
return true;
}

queueOutput("{{gm}}<p>That name seems kind of...short. I'm not going to steal your identity, I promise.</p>");
return false;
}
);

This interrupt is a little more complex than the previous example. It has an initialization function (which prompts the player for their name), and it has a return value rather than manually deregistering itself. This communicates to the NLP whether the interrupt is done. If the interrupt returns false, it will be executed again on the player’s next input. This is useful because we want to make sure the player has entered valid input for their name. For now, we’re just checking for a non-empty string, but we could create more complex validation in the future.

When we’re satisfied the player’s input is valid, the interrupt returns true, telling the NLP to continue (either to the next interrupt, or back to regular command processing). During the SRPG intro segment, several interrupts are queued to capture the player’s name, race, class, gender, and preferences.

The first interrupt in SRPG is a simple text interrupt. The rest of the interrupts during the SRPG intro are menus. They present a list of options, and validate the player’s input against the available options before allowing them to continue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ADD RACE MENU TO INTERRUPT QUEUE
NLP.interrupt(
function(){
// Build menu
var menu = parse("{{menu races}}", {'races':shuffle(ECS.getData('races'))});
queueOutput("{{gm}}<p>If you say so. Well, {{player.name}}, what manner of creature are you?</p>"+menu);
},
function(string){
player.race = string;
disableLastMenu(string);
if(ECS.isValidMenuOption(ECS.getData('races'), string)) {
return true;
}

enableLastMenu();
queueOutput("{{gm}}<p>That wasn't one of the options. Try again, you rebel.</p>");
return false;
}
);

Note the check on line 11, which validates the player’s input. If the input is invalid, the menu is displayed again. A fair bit of work is done on line 5 to build the menu output, which I’ll cover in another post. In general, a menu option includes some text, and a command to execute when the option is clicked.

Dialogue Trees

Dialogue trees function similarly to menus, in that they present a clickable list of options that cannot be ‘escaped’ until certain criteria are established. You can think of a dialogue tree as a set of interlinked menus with more advanced command interrupt logic (to determine which menu options are currently available, and therefore which player inputs are currently valid). The code for handling dialogue trees is a bit too extensive to present here. When a conversation with an NPC is initiated, the Social module handles the process of finding the correct conversation nodes and setting NLP interrupts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function() {
var conversation = this;
var nodes = this.getCurrentNodes();
var menu = [];
var node = this.currentNode;
for(var n in nodes) {
menu.push({'text': nodes[n].prompt, 'command': nodes[n].key, 'subtext': '<small>'+(nodes[n].visited ? '(Visited)' : '')+'</small>'});
}
menu.push({'text':"<i>(EXIT)</i>",'command':'exit','subtext':"I'm done talking."});

NLP.interrupt(
function(){
node.callback();
queueOutput(parse("{{menu options}}", {'options':menu}));
},
function(string){
if(string == 'exit' || conversation.doTopicNode(string)) {
return true;
}

enableLastMenu();
queueOutput("{{gm}}<p>There is no response.</p>");
return false;
}
);
}

This general-purpose block of code populates a menu with dialogue options, then sets the interrupt. Within the interrupt, we check for valid player input (either selecting a dialogue option or exiting the tree) and process the selected node (if any). Lastly, if the input was invalid, we return an appropriate response (later on this will probably become a callback, so different NPCs can give different “no response” responses).

Topic for tomorrow: the game is a mod of itself.