Game Jam Day 2

Well, I kept you waiting for an eternity and a half. Day 2. Finally. (Only like 7 more to go. Woot.)

On Day 2 I started the very basics of the text adventure. As of then my game was a canvas tag on a page. What I did is put a textarea and an input box below the canvas tag so that I could get some output while still having the game be intact.

Game above, textbox below.

(Like any good person, I take my inspiration from Homestar Runner. No ye flasks to get here, though.)


Now that we had a way to get words in, we needed a way to get actions out. Generally speaking text adventure game commands followed the format of [VERB] [ADJECTIVE] [NOUN]. More or less, we want to [VERB] on the [NOUN] that is [ADJECTIVE]. For example, we could to GET [VERB] YE [ADJECTIVE] FLASK [NOUN]. In addition, we'd have to deal with extra words. Take PICK UP TRINKET vs. PICK TRINKET. PICK is the [VERB] and TRINKET is the [NOUN], but UP's only purpose is to make the sentence sound like something a human would say. How the heck do we do all that?

Essentially, what we need to do is figure out what [VERB] and [NOUN] are. Let's start with [VERB]. Typically [VERB] is always the first word in the sentence. That's easy enough to get. Let's take this phrase an an example:

GO TO SOUTH

The code reads the command and strip out the first word, "GO", by converting the string into an array of words and then picking the first one. We end up with this:

["GO" < [VERB] , "TO", "SOUTH"]

Simple enough, right? Here's the screwball. What do we do with the verb? How do we associate the verb with code?


One way you could do this is by just having a giant if-else list. Something like this:

var verb = words[0]; //Verb is first word
if (verb === "GO") {
    // code for GO
}
else if (verb === "GET") {
    // code for GET
} ...
else {
    WriteText("You can't " + verb);
}

That totally would have worked, except for one problem. I wouldn't be able to call the code without going through the command parser. I needed a way to put each bit of code in a separate function. (Well, actually, I don't think I ever needed to do that ever, but it was early enough in development that I wanted to plan for anything. In fact you could argue that I shouldn't have done this, because you'd effectively need to parse the nouns anyways, but it's a bit late now.)

One way to do that would be to make every command a separate function. The problem with that is that you then have random functions with names like "GO" and "GET" that you can't use for anything else. (That's called Namespace Pollution, btw.)

I needed all the functions in an object. This is easy enough in JavaScript. You just make an object like you would any other object (like var ObjectName = {key1: val1, key2: val2}), except instead of having the values be numbers or strings, they're functions.

The problem with THAT is that you can only get the value of an object's key if you know its name. So, for instance, to get key1 you'd type ObjectName.key1. What you could not do, however, is say var key = "key1"; var ObjectName = ...; ObjectName.key and expect to get key1's value.

Here's where we get a bit fun and hackish. Let's look at arrays. You don't know the name of anything in an array. Therefore, you can't say 'Array.element' and expect to get anything. What you do instead is tell an array to get the value at index 'element' like so: 'Array[element]'. For arrays, 'element' is a number. But for objects, 'element' is a string, corresponding with the key's name. Therefore, while you can't say 'ObjectName.key' and get key1's value, you CAN say 'ObjectName[key]', because the [] expects a string (in quotes), and not a name, so it gets the value of key, which is a string, and uses that. It's funky as funk, but it works beautifully.

Here's what that command object ends up looking like.

var CommandFunctions = {
    GO: function (words){
        // code for GO
    }, 
    GET: function (words){
        // code for GET
    },
    ...
}
//...
command = "GET";
words = \["YE", "FLASK"\];

// Call the command "GET" with the words "YE FLASK".
CommandFunctions\[command\](words)


Slight tangent time. I never covered the "hero" variable, and that's pretty darn important. It contains these values:

var hero = {
    w: 32, h: 64,       // Image width and height, for collision detection
    x: 40, y: 288,      // Player's current X and Y position
    dX: 0, dY: 0,       // Player's movement in pixels per second
    maxdX: 3, maxdY: 5, // Maximum X and Y speed
    dir: 1,             // Player's direction. 0 = left, 1 = right
    currentRoom: 0,     // Room the player is currently in
    monstersCaught: 0,  // Holdover from old code. Monsters killed/ice cream cones eaten.
    maxFrames: 1, animSpeed: 1, animFrame: 1, image: null, // Animation stuff. Will cover later.
}

In order to look up the exits in the current room, we look at the room the player is currently in, or hero.CurrentRoom. We then look up that number in the room[] array, and that should be all the details for our current room.)


We've got a verb. Now what the heck do we verb on? We need a noun. The problem with doing that is that different commands expect different types of nouns. For instance, "GO" expects the name of an exit to go to, but "GET" expects the name of an object to pick up. Because of this, it's easiest just to make the function deal with getting the noun. (We could have also made the function somehow indicate what type of noun it wanted and have the code beforehand grab that noun, but it's not something I thought of at the time.)

Sticking with the "GO TO SOUTH" example, we need to find the South exit and go to it. How do we do that? It's not too hard. We have an array of words, "tokenList". (A token is an individual word or element that is distinct from the rest.) We also, for each level, have an array of exits, "rooms[hero.currentRoom].exits[]".

So, presumably one of those words MUST be the name of a room. (If it isn't, then the user made a typo.) So, for every word, we look at the room exits array. It looks something like this:

exits: [
    {
        x: 640,       // X position of exit in game
        y: 0,         // Y position of exit in game
        w: 1,         // Width of exit in game
        h: 400,       // Height of exit in game
        to: 1,        // Room no. exit leads to
        name: "south" // Name of exit for text adv.
    }, ...
]

What we need to look for is the value of j when tokenList[i] === exits[j]. The code (I know, so much code. Trust me, it's worth it.) looks like this:

currExits = room\[hero.currentRoom\].exits;         // Simplify code for later
for(var i = 0; i < inputTokens.length; i++){      // For each input token
    for(var j = 0; j < currExits.length; j++){    // For each exit
        if(inputTokens\[i\] == currExits\[j\].name){  // Given name and exit name match
            Watson.currentRoom = currExits\[j\].to; // Set current room to exit's "to" key
            return;                               // Break out of loop and function
        }
    }
}

(More tangents. "Watson" is the name I gave to the text adventure hero. I made a whole plot and everything, although I'll admit it's not particularly good. It really needed more time to sort itself out. But I'll get to that later.) The actual code converts both names to lowercase too, but that's the jist of it.


To put all THAT into perspective, let's go to the original input.

GO TO SOUTH

The first word is "GO", so we call the "GO" function with all the other words as the argument.

go: "TO SOUTH"

Then it looks through every word. "TO" is not an exit, so it gets skipped.

go: "SOUTH"

However, "SOUTH" is. So the code ends up setting your current room to the room "SOUTH" corresponds to.


Also, that day I decided to dump Notepad++ in favor of Visual Studio Code. Pretty much the only reason I dumped Notepad++ in the first place was because the functions panel on the side didn't support JavaScript for some reason. (Or at least not the crazy functions-in-arrays JavaScript I was doing, anyways.) For the record, Visual Studio Code didn't support those either, but I kept it around mostly for IntelliSense, which helped me pick out errors before running it in the browser. Trust me, that's a really nice feature. Also it integrated with the Git repo I set up in that folder, which was cool but not particularly useful. (I couldn't, for instance, view previous versions or anything like that.)

...I guess that wasn't shorter at all. Which is strange, because all that only took an hour. (Although to be fair a lot of this spilled over into the next day.) But hey, next time we get to deal with adjectives and multiple nouns. Oh boy.