Image by Giacomo Zanni from Pixabay

How to Create a Text Adventure Game Inventory in JavaScript

Use class inheritance to define and describe game items in an interactive fiction premise

Giannis G. Georgiou
The Startup
Published in
9 min readMay 18, 2020

--

Inform 7 was where I started coding. I learned the most fundamental concepts of programming by building interactive fiction exercises. For loops, ifs, and variables—they were all right there, at the core of those games. So where objects, classes, and subclasses.

I love sharing as I learn, especially through stimulating and playful examples. While teaching myself JavaScript, as soon as I learned about ES6 classes and inheritance, I thought of Inform’s object oriented programming and its kinds—that’s how classes are called in I7. And the question came naturally:

How can I simulate an adventure game’s inventory of items and descriptions, using JavaScript?

Overview

Let’s program some game items and a simple inventory, using JS objects and inheritance.

Specifically, we’ll create an Item parent class, which has two properties: a name and a state. The state property is an integer describing whether the item:

0: is out-of-play (destroyed or not yet introduced)
1: is somewhere in the game world, but not yet found by the player
2: has been handled by the player—e.g. taken and then dropped
3: is carried by the player.

This obviously is an improvised—and arbitrary—system and it may not fully suit a real game, but you can tailor it to your own game’s needs.

Then, we will create two subclasses, Weapon and Wallet, which will inherit the Item’s properties of name and state. Moreover, the Weapon class will also have a damage factor and the Wallet class will have the amount of money it contains.

Finally, we will create a console log that gives a detailed report about all the items we’ve created.

So, let’s go!

The Code

Creating the Classes

We start our code with the Item class:

class Item {
constructor(nam, stat) {
this.name = nam;
this.state = stat;
}
};

People usually write constructor (name, age, email)and then use the same words for the "this" declaration, as in: this.name = name; this.age = age; etc.

I am a noob and I need to see which word corresponds to what. Therefore, until I get more confident, I will prefer differentiating the arguments passed to a function from the property names.

After the Item, let’s create the subclasses. We use the extend keyword to declare that the Weapon and Wallet are subclasses of Item:

class Weapon extends Item {
constructor(nme, dmg, stt) {
super(nme, stt);
this.damage = dmg;
}
};

class Wallet extends Item {
constructor(nme, gld, slv, cpr, stt) {
super(nme, stt);
this.gold = gld;
this.silver = slv;
this.copper = cpr;
}
};

Notice that the Weapon class has the damage property, while the Wallet class has gold, silver, and copper coin properties. (Some or all of them can be zero, of course.)

Also, notice the use of super() and its arguments. Super() refers to the parent class of Item. The arguments we feed super() in Weapon or Wallet, are fed to the Item. The line:

constructor(nme, dmg, stt) {
super(nme, stt);

can be interpreted as “to make this object, I will give you 3 values, but 2 of them are to be passed to the parent class.”

Isn’t inheritance wonderful?

Creating the Actual Objects

Using the classes we’ve made, let’s create some items for the game world:

var worldStuff = [
sword = new Weapon ("two-handed sword", 5, 1),
axe = new Weapon ("battle axe", 4, 2),
pouch = new Wallet ("leather pouch", 1, 0, 1, 3),
liquorice = new Item ("liquorice", 0),
helmet = new Item ("rusty helmet", 3),
boots = new Item ("snake-skin boots", 3)
];

sword.description = "Everything a great warrior needs. Or is it 'a great worrier?'";
axe.description = "A double axe with iron blades and a wooden handle.";
pouch.description = "Your father's pouch, made of Spanish leather.";
liquorice.description = "A half-eaten piece of liquorice.";
helmet.description = "Why do you keep stuff like that anyway?";
boots.description = "Made of real anaconda skin—or so said the merchant.";

Not to overload the constructors with parameters, I left the descriptions to be filled-in independently, after the new objects declaration.

If you get confused with which number corresponds to which parameter, just go back to the classes declaration and look at the arguments in the parentheses. For example, the sword object has the name “two-handed sword,” damage: 3, and state: 1 (meaning it’s somewhere in the game world).

I put all the items in an array called worldStuff, just to have them all together. I can also create an inventory array that will contain all the names of the carried items:

var inventory = [];

for (let i of worldStuff) {
if (i.state === 3) {
inventory.push(i.name);
}
};

console.log(inventory);

The for loop runs through all the items in the worldStuff array and adds the once carried (having a state of 3) to the inventory array, using the push() method.

The inventory array may contain the objects themselves—and not just the names. It depends on how you want to deal with the items in the context of a real game. For the sake of our example, we’ll just leave it as it is.

Dealing With Money

The leather pouch’s description does not include the amount of money it contains. To have the game report the amount of coins to the player, we will create a specific function:

function countMoney(obj) {
let reply = `Your ${obj.name} contains `;
if (obj.gold) {
reply = reply + `${obj.gold} gold`;
if (obj.silver && obj.copper) {
reply = reply + ", ";
} else if (obj.copper || obj.silver) {
reply = reply + " and ";
};
};
if (obj.silver) {
reply = reply + `${obj.silver} silver`;
if (obj.gold && obj.copper) {
reply = reply + ", and ";
} else if (obj.copper) {
reply = reply + " and "
};
};
if (obj.copper) {
reply = reply + `${obj.copper} copper`
};
if (!obj.gold && !obj.silver && !obj.copper) {
reply = reply + "no";
};
reply = reply + " coin"
if (obj.copper > 1 || (obj.copper === 0 && obj.silver > 1) || (obj.copper === 0 && obj.silver === 0 && obj.gold > 1) || (!obj.gold && !obj.silver && !obj.copper)) {
reply = reply + "s"
};
reply = reply + "."
console.log(reply);
};

Don’t be overwhelmed by the length of this function. The reason it’s so long is the variety of cases that it gets to deal with. For example:

> Your leather pouch contains 1 gold, 4 silver, and 1 copper coin.
> Your leather pouch contains 1 gold coin.
> Your leather pouch contains 1 gold, 4 silver, and 2 copper coins.
> Your leather pouch contains 1 gold and 1 copper coin.
etc.

The logic behind it:

We start a string (called reply) and add to it, step by step, as we check for gold, silver, and copper. In this explanation, I have noted the spaces with underscores for the sake of clarity:

(1) Start of the reply: Your_pouch_contains_...

(2.1) We check for gold. If there are any gold coins, we add … (number)_gold... to the string of our reply.

(2.2) Taking the gold as a given, we also check for silver and copper. If there is only one of those two, we add … _and... If there are all three kinds of coins, we continue with a ... ,_...

(3.1) We check for silver. If yes, we add … (number)_silver...

(3.2) In the same way, with silver as a given, we check for the rest. If there is also gold and copper, we add …,_and_...—to use the Oxford comma. Otherwise, just ..._and_...

(4) Copper is next. If there is copper, we add … (number)_copper... to the string of our reply.

(5) Let’s not forget the case of no coins at all, to which we add … no... to our reply.

Then, we add the word coin, singular, along with an if clause including all the cases that it will require an additional s, for plural. That's when the last mentioned kind has a plural amount of coins:

(5.1) if there is more than one copper coin or
(5.2) if there is no copper and there is more than one silver coin or
(5.3) if there is no copper or silver and there is more than one gold coin or
(5.4) if there are no coins at all.

(6) Finally, we add the sentence’s . period. Phew!

We need to be careful with the spaces. Running a few tests with different amounts can help spot missing or double spaces, fixing any mistakes.

I admit that the Oxford comma makes things a little more complicated, but a little bit of style never hurt anybody. Try changing the coin amounts to see different results.

Examining the Items

We can now create a function that takes an object as an argument and returns the item’s description, the damage it causes (if a weapon), and the amount of coins it contains (if a wallet).

Of course, in a real game, there would be issues of scope (whether the item is visible, reachable, etc). For the sake of this example we will “examine” even the items that aren’t in the game world (state: 0).

To see if the item is a weapon, we include an if conditional that checks whether “damage” is one of the object’s properties. If true, we get a report on the amount of damage it can do.

To deal with the states, we use the switch statement for the various values state can take.

function examineItem(obj) {
console.log(obj.name.toUpperCase());
console.log(obj.description);
if ("damage" in obj) {
console.log(`The ${obj.name} can cause ${obj.damage} points of damage.`);
};
switch (obj.state) {
case 0:
console.log(`The ${obj.name} has been destroyed (or not appeared in the game yet).`);
break;
case 1:
console.log(`The ${obj.name} is somewhere in the game world, but you haven't found it, yet.`);
break;
case 2:
console.log(`You have handled (but are not carrying) the ${obj.name}.`);
break;
case 3:
console.log(`You are carrying the ${obj.name}.`);
break;
default:
console.log(`You have no idea what the state of the ${obj.name} is.`);
};
if ("copper" in obj) {
countMoney(obj);
};
};

To see if the item contains money, it’s enough to check if it contains only one of the “copper,” “silver,” or “gold” properties.

So all that’s left now is to run the examining function for several objects:

examineItem(sword);
examineItem(axe);
examineItem(pouch);
examineItem(liquorice);
examineItem(helmet);
examineItem(boots);

We get:

TWO-HANDED SWORD
Everything a great warrior needs. Or is it ‘a great worrier?’
The two-handed sword can cause 5 points of damage.
The two-handed sword is somewhere in the game world, but you haven’t found it, yet.

BATTLE AXE
A double axe with iron blades and a wooden handle.
The battle axe can cause 4 points of damage.
You have handled (but are not carrying) the battle axe.

LEATHER POUCH
Your father’s pouch, made of Spanish leather.
You are carrying the leather pouch.
Your leather pouch contains 1 gold and 1 copper coin.

LIQUORICE
A half-eaten piece of liquorice.
The liquorice has been destroyed (or not appeared in the game yet).

RUSTY HELMET
Why do you keep stuff like that anyway?
You are carrying the rusty helmet.

SNAKE-SKIN BOOTS
Made of real anaconda skin — or so said the merchant.
You are carrying the snake-skin boots.

Backstage and Trivia

Dealing With Money

At first, I tried to express money as a float, from which the integer part would represent the silver and the decimal one the copper.

A couple of problems, though. First, it isn’t possible to have 3 kinds of coins, gold, silver, and copper.

Second, separating the decimal part from the integer part is tricky. It requires the Math.floor() method and I couldn’t really understand why it gave me some of the results it gave me.

The other idea was to declare money as a string that contains the actual units of gold, silver, and copper coins, as in: “23g145s0c.”

It didn’t take me long to realise that having three properties (this.gold, this.silver, and this.copper) was the simplest solution.

Dealing With Subclasses’ Arguments

Until recently, I didn’t know how to pass arguments from the child to the parent class.

’s article ES6 — classes and inheritance had the answer: whatever arguments are to be passed to the parent, we have to put in the parentheses of super(), as we did above:

class Weapon extends Item {
constructor(nme, dmg, stt) {
super(nme, stt);
this.damage = dmg;
}
};

Therefore, the subclass constructor parentheses include all the arguments we want to pass in the subclass. Those not in the super() method, we can define through the this keyword, as usual.

Conclusion

I hope you found this example helpful and inspiring. Do you have any good suggestions for making things more efficient? Did I miss something obvious to you?

Educate me!

--

--

Giannis G. Georgiou
The Startup

Excited about telling stories through various media. Filmmaker and Developer Apprentice—teaching myself to code and sharing the XP.