Weekly Update #3

Mostly side projects this week. Big new project starting at work, that will be eating up most of my weekly dev time. Evenings and weekends, here I come.

Changelog:

  • Continued refactoring dialogue trees.
  • Side Project: Built Numenera card generator in C++. Sent sample to be printed.

Known Issues:

  • Nothing new

StupidRPG: Callbacks

Callbacks are functions on an object, typically executed based on an event somewhere in the game. For example, each time a game ‘tick’ occurs, an AI callback is triggered on every living thing (allowing them to move, attack, or take other actions). SRPG has a couple types of callbacks, which I hope to consolidate in future refactoring.

Callback Types

The current callback behavior is slightly convoluted, due to changes mid-development. Initially, each object could only have one callback of a given name, which proved to be too limiting. It meant that I couldn’t stack callbacks to allow objects to inherit behaviors and expand on them.

The quick solution to this was to use an array of functions, and instead of setting the callback directly, add it to the end of the array. This works fine for the most part, but the code feels a bit clunky for more advanced use cases, and due to the mix of new code and old code, I have to perform several additional checks before executing callbacks.

Refactoring

One of my refactoring tasks is to clean up the callbacks in a few ways:

  • Create a Callback prototype which all callbacks inherit from
  • Standardize all object callback fields to use an array (no direct references to single callbacks)
  • Standardize callback return values to let the caller know definitively how/whether the callback was handled

Reorganizing will allow me to add more advanced callback functionality as well, such as specifying event filters for callbacks, prioritizing callbacks, and allowing callbacks to cancel events entirely (not allowing other callbacks to run).

Side Project: Power Cards

One of the many shortcomings of my technology toolkit is C++. I’ve tinkered with it here and there over the years, but the vast majority of my time and energy has gone into easier languages such as C#, Java, and PHP. I set out to rectify that recently by 1) borrowing a copy of The C++ Programming Language from a generous friend, and 2) diving into a hands-on project well beyond my capabilities. If learning by beating your head against a compiler was a valid way of learning programming, I’d be a Computer Science professor.

The Project

I currently GM two systems: Dungeons & Dragons 5th Edition, and Numenera (a more recent system by former D&D designer Monte Cook). For D&D, Wizards of the Coast had a set of “power cards” produced for every spell in the game. They’re standard playing card size, excellent quality (sturdy and laminated), and big time-savers at the table, especially for newer players or anyone with an inordinate number of spells (hello Wizard!). For most spells it’s no longer necessary to look them up in the rulebook (a few have too much information to fit on a card).

Numenera doesn’t have power cards. Maybe they’ll get around to it at some point (they’ve released card decks for random items and XP). In the meantime, it seemed like a fun project. The basic idea is:

  1. Copy the powers (Esoteries for the Nano type/class, Tricks of the Trade for Jacks, and Fighting Moves for Glaives) from the source book(s).
  2. Organize the powers in Excel (separating fields for character type, power tier, title, description, cost and so forth).
  3. Export to CSV.
  4. Import the CSV in my C++ program, loop through the entries and output nicely-formatted card images using ImageMagick.
  5. Print the cards, either on cardstock at home or via a print service / shop.

Step One: Learn C++

This process will continue through every other step and beyond. C++ requires more attention to things like memory management, use of references/pointers, and in general has a less friendly syntax than a language like C#. I have several blocks of code that work for reasons still unknown to me, and several things I tried previously failed to work due to equally mysterious causes.

Step Two: Learn ImageMagick

I spent way more time on this step than I should have. Most of it can be explained by my inexperience with C++ (and with native Windows development), but it seems like there are a few areas where ImageMagick’s actual behavior doesn’t match its documentation. I eventually got it compiled and working with my program (while learning a lot about lib files and DLL loading along the way).

Once I had the library working, drawing elements to an image and saving it was surprisingly easy. Working with text, however, is another story. Among other issues, ImageMagick doesn’t seem to have built-in support for text wrapping. Since I need to write decent-sized chunks of text and I don’t feel like using a monospaced font, I ended up writing what may be the worst text wrapping function of all time.

Step Three: Write the Worst Text-Wrapping Function of All Time

Ok. So there’s no built-in text wrapping. There’s also no built-in way to measure the size of a piece of text. I think there’s a non-ImageMagick way to get the font data that would let me do that, but at this point my head is already full of too much other new information. So I decided to work around the issue using only ImageMagick’s functionality and plain C++. I noticed that when drawing text, you can specify a background color, which will draw a colored rectangle behind the text. You can also sample pixels from anywhere in the image, which turns out to be pretty handy.

Here’s how the text wrap works:

  • Set the background color AND the text color to red
  • Write the text onto a temp image
  • Get a pixel at the right side of the image
  • If the pixel is red, the text is too long to fit on the line
  • Cut a word from the end of the text and try again
  • If it’s stupid but it works, it’s not stupid

I did perform some optimizations, as this turned out to be (predictably) slow with longer strings. In particular, I picked a semi-arbitrary character count that I think is unlikely to ever fit on a single line, and I only write/check that amount of text at a time. If I have 800 characters of text to write, chances are no more than about 50 characters will fit on each line (for the font size and image size I’m using). This reduces the number of failed attempts for each line considerably. Each time I write a line, I get the next ~50 characters, move down to the next line and continue.

The function currently lacks a few features (some more important than others):

  • Doesn’t handle overflow off the bottom of the image
  • Doesn’t minimize wasted space at ends of lines (e.g. sometimes it might be better to leave a larger gap at the end of one line to use the next few more optimally)
  • Only handles left or right alignment; I’d like to have the text justified

Step 4: Tidy Up and Print!

With the basic functionality working, I put some time into cleaning up the output, designing some card backs, and doing general cleanup. I’m almost ready to print the actual cards.

Example card

StupidRPG: The RPG

There are many different types of RPGs, and many different types of players. StupidRPG is based on my own experiences and preferences, so there are a few elements I’m prioritizing above the rest in the game. Here’s a small selection of the most significant items.

Self-Expression / Player Agency

Most RPGs give the player a lot of options for how they build their character and interact with the world. In essence, the player feels as if the events of the game are being influenced by their actions (i.e. they are not simply triggering the next pre-scripted segment). Pen-and-paper RPGs offer the most freedom in this regard, but computer RPGs can offer a great deal of player agency as well.

Races & Classes

Race and/or class selection are mainstays of traditional RPGs; one of the first steps in customizing your character and dictating your role in the game. Effects are sometimes mechanical, sometimes merely flavor.

Quests

Quests are ubiquitous in computer RPGs (and common, though often in a more abstract way, in pen-and-paper games). They’re an easy way to give the player direction, track their progress, and offer rewards for specific actions in the world. Plus, what RPG would be complete without an obnoxious fetch quest?

Leveling Up

Becoming more powerful is another nigh-universal trait of RPGs, and StupidRPG is no different. While the game (probably) won’t have XP, it will have a leveling mechanism to expand the player’s capabilities. Whether this means a skill tree, static upgrades, or something else, I’m not yet sure.

Bio: Ranger Bob

  • Alignment: Lawful Good
  • Age: 51
  • Traits: Honest, curmudgeonly
  • Hobbies: Birdwatching, astronomy

Ranger Bob is the watcher and protector of the Forest (the starting region in StupidRPG). He stands vigilant guard from his watchtower, venturing out only to resupply or to deal with emergencies (such as local teens violating curfew at the local hot springs). He doesn’t like to talk much, but for the right adventurer he’ll open up and offer bits of wisdom or general observations about the state of the world relative to the way things used to be.

Bob, son of a baker and a seamstress, hails from the local town of Barony (née Torrensbürg). He has always had a deep affinity for nature, frequently sneaking past the city guards to venture into the deep woods as a child. As an adult, he took over the forest watch after the retirement of the venerable Ranger Karen.

Ranger Bob says:

Hmph.

StupidRPG: Everything is an Entity

The most basic element in SRPG is an Entity, which you can think of as a bag of stuff (where ‘stuff’ can mean a name, a list of things, references to other Entities, or functions). Items, locations, and NPCs are all Entities (plus more stuff).

The basic entity definition (or prototype) looks like this:

ECS: Entity Prototype
1
2
3
4
5
6
7
8
9
10
var Entity = function(){
this.key = ''; // Identifier
this.parent = null; // Parent entity
this.children = []; // Child entities
this.components = []; // Component list, for convenience / searching
this.onComponentAdd = []; // Add component callback
this.tags = []; // Tag list; generally the same as the component list
this.persist = ['parent']; // Raw attributes to persist
this.persistActive = true; // Save objects by default
};

Nothing super exciting here. The base entity mostly defines ‘housekeeping’ items: an identifier, references to parent and child entities, etc. I want to focus on Components right now, as that’s the most important part of ‘everything is an entity.’

Functionality and data associated with game objects are defined by Components. You can think of a Component as a ‘type of thing’. Examples include Place (a location), Region (a group of Places), Living (an entity that is alive), Container (an object that holds other objects), and so forth. Most components correlate to some kind of real-world designation.

The most basic Component is called Thing. When I said ‘everything is an entity,’ I could have instead said that ‘everything is a thing.’ A Thing, of course, is also an Entity, but as a Thing it has a variety of in-world properties.

Things have a bunch of properties included by default, acting as the building blocks of item interaction in the game world:

  • name (e.g. ‘blue flowers’)
  • descriptions (a list of descriptions for different contexts)
  • place (the current location of the thing)
  • spawn (the starting location of the thing)

There are a bunch of additional elements that make up a thing (mostly callback functions to handle basic behaviors: picking up and dropping the thing, adding more components to the thing, which data to save for the thing, etc). Certain basic items in the game don’t need any additional components, but most will have their interactivity and behaviors further expanded.

For example, the Living component adds:

  • hp (hit points)
  • onTick (a callback function, run on each game ‘tick’, allowing the object to take actions)
  • onHit (callback, run any time the object is attacked)
  • onDeath (callback, run when the object’s HP is reduced to 0)

Example: Containers

The Container component is a bit more advanced, as it is actually dependent on the Holder component. Components can specify a dependency list (meaning other component(s) the base object should also have). The Holder component specifies functionality used by both the Container (an object in which other objects can be placed) and Supporter (an object on which other objects can be placed) components.

The Holder component specifies:

  • capacity (how many objects can be held)
  • isTransparent (whether the object can be seen through)
  • canTakeFrom and canPutInto (whether things can currently be placed in/on the holder)
  • inventory (a list of objects being held)

The Container component adds:

  • isOpen (whether the container is open)
  • onOpen and onClose (callbacks, run when the container is opened/closed)
  • onList (callback, overrides a basic Thing callback and controls how the object’s description is printed)

The onList callback is particularly notable, as it changes based on the isTransparent property (from Holder) and the isOpen property (from Container). If the container is open or transparent, its contents are listed when the object is described. Otherwise, only the object itself is described.

The Supporter component also has an onList callback, but it’s much simpler as it doesn’t need to pay attention to transparency or openness.

Component Interactions

In general, there are no hard-coded restrictions on which components (or how many) can be applied to a particular entity. This has advantages (e.g. you can do interesting things like allowing a location to be alive and move around the world) and disadvantages (e.g. whatever happens when you add two components, each of which overrides the ‘onList’ behavior). Interactions can become complex or hard to predict. I was aware of this while building the system, and felt that the disadvantages could usually be worked around, while the advantages were particularly exciting to me.

Weekly Update #2

Stuck on that dialogue node issue. Very odd behavior where multiple dialogue trees (one per NPC) are interfering with each other. Haven’t been able to resolve it yet.

Changelog:

  • Started refactoring the dialogue node callbacks. Current code is hard to debug.

Known Issues:

  • Dialogue node callbacks aren’t working right. Need to refactor.

With Graphics & Sound

Short post today. I’d like to give a quick overview of the ‘graphics and sound’ portion of the statement ‘StupidRPG is a text adventure with graphics and sound.’

Sound

I’m the least musically-inclined person I’ve ever met, so I made the wise choice of marrying a composer. Sound in SRPG includes soundscapes (background noises) and music (the stuff with notes and whatnot). Sound/music can be optionally enabled during the intro portion of the game, and plays via an HTML5 audio element. Music can be cued by entering a new region or by triggering a specific event. Each track can provide settings for volume, looping, and closed captions.

Graphics

This part is still up in the air, but it’s a definite goal for the game. In particular, I’d like to make use of the empty sidebar areas for some nice ambient/background art, varied per location/region. Highlighting key items and NPCs graphically would be a nice addition as well. One of the nice things about the way the engine is built is that actions/input can be triggered from anywhere (for example, clicking on a bush in the sidebar could trigger the LOOK AT BUSH action).

StupidRPG: Natural Language Processing

Language parsing is hard. The parser in SRPG took me a couple months to get working the way I wanted, and it’s a very simple parser compared to others I’ve seen. I’ve previously listed the command patterns (or sentence patterns) SRPG can understand; the list has grown gradually during the course of SRPG’s development, but at this point it’s unlikely to change much prior to the first release. It covers 99% of the patterns I’m interested in supporting.

This will be a long, moderately-technical post.

Pattern List Reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Pattern list
// Cannot start with a modifier
// In order of priority (highest to lowest):
'patterns' = [
'VERB', // inventory
'VERB NOUN', // eat baby
'VERB MODIFIER', // saunter west
'VERB MODIFIER NOUN', // get in closet
'VERB NOUN MODIFIER NOUN', // attack goblin with hammer
'VERB MODIFIER MODIFIER NOUN', // look north through telescope
'VERB MODIFIER NOUN MODIFIER NOUN', // look at bob through telescope
// Overflow patterns
'VERB MODIFIER NOUN MODIFIER TEXT', // talk to goblin about greatest fears
'VERB MODIFIER MODIFIER TEXT', // look north through telescope saucily
'VERB NOUN MODIFIER TEXT', // ask bob about back pain
'VERB MODIFIER TEXT' // talk about floops
];

Patterns are only the tip of the iceberg though. A lot of work goes on behind the scenes to convert arbitrary text from the player into a meaningful action within the game. The end result functions similarly to a lexer/parser in that it processes a string via a set of grammar rules. The parser performs the following steps:

  1. Do basic sanitization (e.g. remove extra whitespace).
  2. If a command interrupt is set, give it a chance to take over.
  3. For each pattern, do:
    • Convert the input string to lowercase for easier parsing.
    • Break the new string into tokens (words).
    • Do sanity checks.
    • For each token in the pattern, try to match to the next available command token(s).
  4. If no VERB was matched, return an error. All commands have to contain a verb.
  5. If a portion of the input wasn’t matched, return an error.
  6. Apply filter restrictions (more on that later), and return an error if the filters fail.
  7. Dispatch the action to the relevant object or verb.

Some steps are more significant than others. I’ll go over the more important/interesting ones in detail.

Pattern Loop (Step 3)

The pattern loop compares each command pattern to the input tokens. Most of the work is done in steps 6-7, but before the process gets there, the input string is segmented and a couple checks are performed. Notably, if the number of input tokens is less than the number of pattern tokens, it skips to the next pattern without doing any more (wasted) work. If a halt has been triggered by the token matching, the pattern processing will stop rather than continuing to the next pattern.

Token Matching (Steps 6-7)

For each token in the pattern, the NLP attempts to match as many of the input tokens as possible.

Here’s a simple example: GO NORTH. This is going to match the VERB MODIFIER pattern (the third pattern). First, however, the NLP will try to use the first pattern, VERB. It will look for a verb called ‘go north’, and upon failing to find one, it will continue to the next pattern: VERB NOUN. This time it will successfully match the verb ‘go’, but it will fail on the second pattern token, NOUN, after failing to find an object called ‘north’. Finally, it will get to the third pattern: VERB MODIFIER. As in the 2nd attempt, it will match the verb ‘go’, and then match the modifier ‘north’ (available for the ‘go’ verb).

Longer input strings take a lot more work. Keep in mind that the NLP will always try to match as much text as possible in each step. A long command like LOOK AT THE MOON THROUGH THE TELESCOPE will end up making the following comparisons for the command pattern VERB NOUN MODIFIER NOUN:

  • Is ‘look at the moon through the telescope’ a verb? No
  • Is ‘look at the moon through the’ a verb? No
  • Is ‘look at the moon through’ a verb? No
  • Is ‘look at the moon’ a verb? No
  • Is ‘look at the’ a verb? No
  • Is ‘look at’ a verb? Yes!
  • Is ‘the moon through the telescope’ a noun? No
  • Is ‘the moon through the’ a noun? No
  • Is ‘the moon through’ a noun? No
  • Is ‘the moon’ a noun? Yes!
  • Is ‘through the telescope’ a modifier? No
  • Is ‘through the’ a modifier? No
  • Is ‘through’ a modifier? Yes!
  • Is ‘the telescope’ a noun? Yes!

If you’re feeling cross-eyed, don’t worry, that’s perfectly normal.

The verb matching function is listed below. The functions for matching verbs, nouns, and modifiers all work in basically the same way. They get a subset of the provided tokens, starting with all of the tokens, and shrink the set until they get a match or run out of tokens.

NLP.matchVerb()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'matchVerb' = function(string) {
var tokens = string.split(' ');

// Loop through token list, try to parse longest first (most tokens)
for(var i = tokens.length; i > 0; i--)
{
var verb = tokens.slice(0, i).join(' ');
var action = ECS.getAction(verb);

if(action != null)
{
return {'match':action,'string':tokens.slice(i).join(' ')};
}
}

return {'match':null,'string':string};
};

On line 8, the function asks the ECS for a verb with the given alias. Most verbs have multiple aliases. Here’s the list for the ‘look’ command: ‘aliases’:[‘look’,’l’,’look at’,’peer’,’glance’,’inspect’,’examine’,’x’].

If a verb was found, the match is returned in two parts: 1) the verb object, and 2) the unmatched remainder of the text. In the above example sequence, matchVerb would return ‘the moon through the telescope’ along with the ‘look’ verb.

Eventually, the NLP matches all the input tokens or determines that not all tokens can be matched. The latter case leads into step 9.

Partial Comprehension (Step 9)

Good feedback is important in the input processor. It should always be as clear as possible to the player why a command failed, so they can resolve it with minimal fuss. Let’s look at two examples:

  • EAT THE CAMEL
  • GO NORTH WHILE CARTWHEELING

The first example is handled by the NLP directly. A generic response is provided, because the verb ‘eat’ was understood, but ‘the camel’ wasn’t identified as an object. The response will be: I understood everything up until ‘the camel’. You want to eat, plus something.

Ok, that’s a good start. The phrasing is a little clunky in this context (probably in most contexts, honestly), but it gets the message across. The player should probably verify that there is indeed a camel in the current location. We can do better though. The NLP allows verbs to specify their own callback for handling failed inputs.

The second example uses the ‘move’ verb (‘go’ is an alias for ‘move’, and ‘north’ is one of the available modifiers). Entering the command above will produce this response, custom-tailored by the verb itself: I understand you want to go somewhere, but I don’t know how to go (while cartwheeling).

Better.

Filters (Step 10)

I’ll have to save a more thorough discussion of Filters for another post. The basic idea is that the NLP’s parsing can be interrupted if certain conditions are met. For example, the LOOK and TAKE verbs have a ‘darkness’ filter applied to them. If the current location is dark, the filter fails (canceling the action).

Dispatching Actions (Step 11)

Alright. Tokens are parsed, command patterns attempted, filters passed. We’ve got a verb, nouns (maybe), modifiers (maybe). Time to actually trigger the command. If we parsed any nouns, we also decide which one is the target and try to hand control off to it. In general, most actions in the game can be customized per-object. If the object doesn’t provide a callback for the verb, or the object’s callback declined to handle the action, control is instead handled to the verb’s default callback.

In either case, an object containing the matched noun(s), matched modifier(s), and the full input string, is passed to the callback.

That’s it. The NLP’s job is done.

Areas for Improvement

I don’t plan on doing any major refactoring of the NLP in the near future. I’ve already spent more time on it than I should, and it handles pretty much every type of useful command I can think of right now. That said, it has a few notable shortcomings and areas for improvement.

Precedence: if the player enters a command like TALK ABOUT SELF (which matches the very last command pattern in the list), the NLP will first attempt to match the command against every other pattern in the list. It’s designed to ‘fail fast’ if the string can’t possibly match the current pattern, but it still feels a bit inefficient to me. I can’t justify more time tinkering with it right now, as it works fine I haven’t identified any performance issues. Just a feeling there’s probably a better way.

Action Targets: currently, the NLP interprets the first noun matched as the target of the action. This leads to problems with commands like LOOK THROUGH TELESCOPE AT BOB vs. LOOK AT BOB THROUGH TELESCOPE, which will interpret the telescope and Bob as the target of the command, respectively. When control is passed to the telescope (first case), the command works fine. When control is passed to Bob (second case), the action will fail (or at least not give the expected result). This is a tricky one to fix.

Disambiguation: the NLP does not currently perform disambiguation when two objects have similar or identical names. The classic Infocom games display a message like “Do you mean the red ball or the blue ball?“ and then interpret the player’s next input as a clarification. Hmm, that sounds a lot like a command interrupt…

Complex Identifiers: once again, the Infocom parser has some cool tricks. For certain verbs, it allows lists of objects to be parsed (useful for picking up multiple items, for example). Advanced cases like TAKE ALL EXCEPT HAMMER AND SCREWDRIVER are understood. Raw text and numbers can also be used mid-input, whereas in SRPG any non-standard text is handled in the ‘overflow’, which always comes at the end of the input string.

I could go on, but at this point I’m just listing all the really smart things Infocom and Inform do that SRPG doesn’t.