Dark Mode in Clubhouse is here! Learnmore
Software Development

Writing Object Oriented JavaScript ES6 Modules, using a Text RPG as an example

Johnny Dunn
> Please enter a command.
> write

What would you like to write?

> help write
novel
manifesto
distasteful tweet
Necronomicon
tutorial

> write tutorial

Loading, please wait…

In object-oriented programming, programs revolve around “objects” which define the type of a data structure as well as the procedures (or methods) that can act on that data.

Succinctly put, objects have data and functions to manipulate data. The relationships and interactions between well-modelled objects will allow us to create systems of great complexity, and allow us to increase that complexity while keeping a maintainable and organized codebase.

Object modeling is a process that will organically break complex features down into granular components, as you’ll see in our example project of creating a text role-playing game.

During this tutorial we’ll walk through creating a functional library that will make use of object oriented design to craft a full-fledged text adventure, or otherwise known as a work of interactive fiction, with just a few hundred lines of code. This is an example of a system with many moving parts that will require some thought to the designs of the components, but with good use of abstraction, encapsulation, and other object-oriented concepts, we can see a surprising amount of intricacy capable with a relatively few number of classes.

We’ll go through how to write JavaScript classes — everything in JavaScript, including classes, is an object — with ECMAScript 6 (ES6) syntax. ES6, a JS specification released in 2015, allows us more streamlined and accessible syntactic shortcuts for many things, like writing classes. You’ll learn how to prepare and export scripts as modules for reusability and for running in the browser (this will involve minifying and bundling up all our dependencies), something that is now a standard process in modern web development.

Before the end of the tutorial, you’ll have this nifty demo running in your browser!

Although this will be a long read, you’ll delve into understanding the mechanics of creating a text role-playing game from scratch, one that can not only deliver a full narrative experience, but also provides a flexible tool for designing custom player interactions—precisely due to the object-oriented / abstracted nature of our modules.

By the end, you’ll see how object oriented programming naturally allows for powerful versatility and extensibility, resulting in endless reusability of our components, and for us to build new systems easily on top of existing ones, which is perfect in procedural, dynamic applications like a role-playing game. This, of course, is all assuming that our codebase has been written well with object-oriented principles in mind.

An introduction to object-oriented design

Here’s a brief review of the major concepts in object-oriented design:

Encapsulation is the idea that an entity’s attributes (data) are enclosed within that entity, and so access to those attributes is restricted outside of the entity (unless the developer purposefully exposes properties publically outside the class). Encapsulation goes entirely hand-in-hand with proper abstraction.

Inheritance is when objects are able to inherit properties from other objects or classes, or in other words, objects can be subclasses of other objects. This is a key principle in helping us keep code modular and loosely coupled. You’ll see this in practice towards the end of the tutorial, when we use methods in one of our classes to create and store our game content (we’ll be creating new game objects as subclasses of some of our core components).

Abstraction is focusing on the essential or required properties of an object, meaning that when we design our classes, we only include the relevant attributes that allows the class to act as a “blueprint” for instantiating objects of that type of class. In other words, objects are created by being instantiated from a class, so classes only contain the core attributes their objects need to know about.

Polymorphism is the ability to create an object or entity with more than one form. Classical examples of polymorphism are more obvious in statically-typed languages like Java or C#, but an applicable example in JavaScript is having a function be able to do different things based on the parameters passed to it. We’ll make some of our functions in our classes with polymorphism in mind where it seems sensible, generally meaning when it makes code cleaner.

Code that follows strong object-oriented design is easily reusable and expandable, and in this example, will allow us to create new text adventures with the same gameplay mechanics by editing a single script/file.

An introduction to the text adventure medium

Text adventures are one of the oldest forms of video games, and helped pioneer the adventure genre in gaming. Some of the most well-known examples include Adventure, Zork I, The Oregon Trail (the original), and The Hitchhiker’s Guide to the Galaxy (the game), with a few modern examples being Violet and Shade.

Works of this form can be seen as a product of video gaming as well as a creation of literature, which is a standout and timeless quality of the medium. These games offered endless amounts of exploration and fun in eras of low graphical computing abilities, and were even accessible to blind players via text-to-speech synthesizers.

The nature of interactive fiction and the gameplay mechanics that encompass it make for a perfect case of object-oriented design. Although we won’t get to these things in this tutorial (it’s already long enough!), consider this example: If we have an Item class with generic properties and methods of interaction, then we can write subclasses like Book or Key as opposed to creating the same types of new objects (which would be repeating coding logic) to represent a different book / key every time.

Let’s define the goals we want to achieve in our proof-of-concept demo. These are typical features you’ll see in a text adventure:

  • Player enters in messages as input for the game
  • Game respond to messages with text of its own
  • Player can interact with objects and environments in the game with basic commands, including “navigation”
  • User can win the game / restart the game

First, the player needs to interact with the game via text input, and the game needs to render responses also in the form of text. And, since text adventures still have physical dimensionality and a sense of space despite having no graphics, the player needs to be able to enter and exit “rooms”, which will comprise the game world. Certain rooms should be inaccessible until the player obtains the required items in their inventory. And finally, just one of these rooms should end the game if the player enters it (they win!).

Here’s an overview of the main classes involved with what features:

  • User input management (Input class)
  • Display renderer (Display class)
  • Game data / decision system (Game, Room, and Player classes)
  • Inventory system (Inventory and Player classes)
  • Item and environment interactions (Inventory, Room, and Player classes)
  • Bonus feature: Make the game be able to load data from a JSON file (Game class)

As you can see, any features related to interactions or decisions based on game state will be more complicated and involve more classes, whereas self-contained features like input and display are encapsulated in single classes. A sign of good object-oriented design is seeing complexity being broken down into modularity, so features with more complex logic should be atomized into more classes.

Now that we’ve reviewed the foundational concepts of object-oriented programming and how they can be applied in building a text adventure game, let’s get started coding!

Starting off easy (writing the simpler classes)

To kick off, we’ll set up user input and get the game’s display working, which will just show text (HTML to be exact, to allow for styling and hyperlinks and showing images if desired).

This is our input.js script:

And that’s all we need there! Client-side code using the library will be responsible for getting and sending messages from an input form (we’ll show that example shortly after reviewing the rest of our initial classes).

For our display.js file (the game’s “graphics”), which again, will be only text in this example but could include HTML elements also, this is what we’ll have.

Notice how we’ve set up the Display class so that it can be constructed with something to render or initialized as an empty display, and also how the show() function takes an optional argument $html.

Allowing show() to do multiple things (render the HTML kept in its internal properties or render HTML given to it) is a good application of polymorphism and of the DRY principle (Don’t Repeat Yourself), which is another pillar in object-oriented programming. We have just one function that can be intuitively used in multiple ways, instead of having both show() and setHTML(). We also have the append() function, which is differentiated from show() because it will not clear the display screen.

You’ll also notice that we even allow the HTML element ID that corresponds to both our Display and Input components to be redefined; they’re not statically hardcoded. We want everything to be as reusable and modular as possible; that’s a definitive feature of object oriented design!

Now that we’ve got the easy stuff out of the way, let’s get into the overall system being created and how the components come together with our main Game class.

Connecting the pieces so far

We’ll have one Game() that manages all the pieces we’ll build, the input, the renderer, gameplay components, and the player character and AI of our game.

At the top of the script, we’ll import our components (we only have the first two right now).

import Display from './display';
import Input from './input'; =

Both Display and Input classes will be instantiated and stored as properties within the Game object (there will only be one Display and Input object per Game), and so they will also be accessible by referencing their properties (via dot syntax) within Game. We’ll also have an optional $datapath property that will define where the game data (JSON file) is located (this will be a “bonus” feature / challenge for you to do, that’ll allow you to easily make new text adventures by editing that JSON file). And, we’ll have an init() function that sets things up, and expose Game.Input with methods like userSend(), enableInput(), and disableInput(). The second part isn’t necessary, but it allows us to do something like call Game.disableInput() as opposed to Game.Input.disable(), which is a little bit more direct and transparent.

export default class Game {

constructor(datapath = '') {
this.Display = new Display();
this.Input = new Input();
this.datapath = datapath;
}

init() {
console.log('Initialized game from: ' + this.datapath);
this.Display.show('<p>Hello world</p>');
}

userSend(message) {
console.log('User sent: ' + message);
this.Input.send(message);
}

disableInput() {
this.Input.disable();
}

enableInput() {
this.Input.enable();
}
}

Making our ES6 modules usable in the browser

Now that we have some functionality built within our modules, let’s go ahead and try using them in the UI / browser. ES6 has brought us extremely nice syntactic features, so we can write classes in much more straightforward ways than in previous versions of JS.

This comes with the caveat of having extra steps in our build process to make our modules work in the browser (by transpiling our code down to ES5 or compatible syntax), but tools like Webpack and Browserify will do this for us.

Remember, you won’t need to bother with Webpack bundling unless you’re making changes to the core scripts / library, but you will have to use Browserify to re-bundle your code when you’re making changes to your game (in the client JS file).

There are many ways and tools available to do this. We won’t go into too many details regarding best practices with bundling modules and transpiling, although here is a very thorough guide in general about the variety of module types and options in the JavaScript ecosystem.

For the purposes of this example, we’ll use the popular webpack-library-starter published on GitHub. This will bundle our ES6 files with Webpack and export them into UMD format, allowing our code to work anywhere, whether it’s powering a Node backend or included with a <script> tag (without any server running).

But before we do that, we need to add one more file to be able to use our scripts as a library as intended. Create a file called index.js and copy and paste the following:

This will allow us to require the directory containing the generated library file (in /lib/text-rpg-engine.js) and use our Game class directly with its API, which we will do in our main client-side JS file (main.js), shown below. Both the main.js and text-rpg-engine.js files must be in the same directory.

const game = require('./text-rpg-engine');

game.init();

// Send user input to our game (on pressing 'Enter' in the form)
document.getElementById('input').addEventListener('keypress', function (event) {
if (event.keyCode === 13) {
event.preventDefault();
game.userSend(document.getElementById('input').value);
document.getElementById('input').value = '';
}
});

As you can see, the code responsible for listening and sending the user input to the game will be handled outside of the ES6 modules, meaning that they contain less dependencies and are more “portable”.

You’ll also see we’re using ‘require’ to import our library, but, require() is not available in browsers by default, so we’d have to refactor our scripts to use an external tool like require.js, or incorporate something like browserify in our build process, which will bundle up all your JS code and dependencies, allowing require() to work in the browser the same way it does in Node.

Let’s go with browserify, as that’ll give us the smallest and most optimal file size we can achieve!

After installing browserify globally with npm install -g browserify we can get our code running by bundling it with this command: browserify main.js -o bundle.js. This command bundles the main.js file we have and outputs the result in the bundle.js file, which will be what our index.html file references.

Now we just need to make the index file, making sure to include our linked input and display divs, and a reference to the minified and bundled JS code. It should look like this:

Open up the index.html page in your browser and try it out, no running server required! We have the bare bones of our text adventure game working.

The funner (but harder) classes

Finally, we get to the more exciting (and much more complicated!) parts of our game. Now we’ll actually begin to build out the “game world” and deconstruct core gameplay mechanics. Text adventures still have physical dimensionality / interactions between the player and the game world, by allowing movement through rooms and having puzzle-solving by interacting with items that can be picked up in the player’s inventory.

Our game data needs to consist of rooms, with items placed in them. And we also need to keep track of what interactions can be performed in the rooms and on what items, and what the results are from those interactions. That certainly sounds like an ambitious and complicated undertaking, but let’s take a look at the bare skeleton of Room, and it won’t look that intimidating.

export default class Room {

constructor(name = '', getText = '', requirements = [], prompts = {}) {
this.name = name;
this.getText = getText; // The text that is displayed when the room is entered
this.requirements = requirements; // Any requirements needed to access the room
this.prompts = prompts; // What are the actions that we can do in this room?
}

Room() has properties of $name, $getText (what’s given to the game to display when the player enters a room or gets an object), $requirements (array of prerequisite items needed to enter the room), and $prompts.

All player interactions (whether on items or within a room) will be defined by our Prompt() class, which will be one of the most complicated objects we have. By themselves, prompts won’t be doing anything, but they will be stored as data and made accessible within a Room object. This will allow us to have multiple prompts triggered by the same keywords, but as long as the prompts are contained in different rooms, they won’t get mixed up in the game. And since Prompts are stored within rooms, item interactions can be differentiated by which room the player is in.

A Room will have various Prompts that’ll allow the player to interact and progress in the game in different ways, since the results of a successful prompt can add new items to the player’s inventory as well as change the current room the player is in. Thus, we can build a fully working game from start to end just by adding new rooms to the game and prompts to the rooms.

Rooms also can have requirements, meaning that a player would need a certain list of items in their inventory in order to be able to enter. Likewise, a Prompt can have item requirements to successfully do that prompt ($requirements).

Setting up the constructor of our Prompt class is easy enough (shown below), and we’ll go through examples of building out various user actions with prompts toward the end.

export default class Prompt {

constructor(name = '', keywords = [], results = {}, requirements = []) {
this.name = name;
// Keywords that can trigger the prompt (make all lower-case by default)
this.keywords = keywords.map(function(v) { return v.toLowerCase();});
// Results that occur when this prompt is successfully triggered;
// the result keys comprise of “successText” (required), "failText" (optional),
// “itemsRequired” (optional), and “roomToEnter” (optional)
this.results = results;
// Any prerequisite items needed to do the prompt?
this.requirements = requirements;
}

Now let’s take a look at our main function in a Prompt: matchKeywords(), which will check the user’s message against the prompt’s trigger keywords, to see if anything matches. If a match is found and if the user has all the prerequisite items required, we return the success results of the prompt. If there are missing items that the prompt requires, we return a fail result which includes a fail message along with the missing required items.

Following along with the comments in the script below, it should be clear what the code’s doing and why.

We’ve got our Item and Room classes and we’ve built out the Prompt class, which is the foundational base of our interaction system. Rooms will have various prompts that can allow for interesting gameplay mechanics (as you’ll see later in our examples).

Let’s finish building out our room class now. Remember, Rooms can have Item requirements, making them inaccessible to the player unless their inventory is correct! So for our enterRoom() function, it must traverse through all the requirements against a list of items passed to the function, to see if the room can be entered or not.

Now things are getting really exciting! We’ve built out the core mechanics that will comprise our gameplay, although we don’t yet have a player class.

The player should have a name (although that doesn’t really matter at the moment), an inventory, and some state keeping track of the current room they’re in. Sounds simple enough and it is! Thankfully, it’s a lot simpler than the Room and Prompt classes, something that demonstrates a beneficial side effect of object oriented programming.

Because we’re encapsulating our features smartly, objects like game rooms and prompts might be complex, but a player and inventory can be more straightforward to write, as they are abstracted away from logic that those components don’t necessarily need to care or know about.

We have a Room class, and our Player will move between rooms to play through the game. All Player has to do, as you’ll see down below, is call the Room class with one line of code and it’s simple after that to try to enter the room, since the logic that determines whether the room is accessible is encapsulated inside the Room class.

Here’s what our Player() constructor will look like:

import Inventory from './inventory';

export default class Player {

constructor(name = '', inventory = new Inventory(), currentRoom = '', startRoom) {
this.name = name;
if (this.name === '') {
this.name = 'player';
}
this.inventory = inventory;
if (this.currentRoom === '') {
this.currentRoom = this.startRoom;
}
this.startRoom = startRoom;
}
}

Let’s go ahead and create our Inventory now, which will be short and sweet, containing very straightforward methods—we only need to be able to add and drop items.

Now that we have an Inventory, we’ll finish building out Player so it uses the inventory, and also is able to enter different rooms.

Notice above, that within our enterRoom() function, we’re relying on the method enter() of the room object passed to the function. So when we call Player.enterRoom(room) in the game code, we need to make sure that room is in fact an object instantiated from our Room() class. (This can be done in multiple ways since JavaScript is dynamically typed, and we’ll be handling this in our library API in the main Game class, by adding methods like addRoom(). You’ll see examples of this when we’re building out the actual content of the game).

We’re finally here! We’ve fleshed out the components we need to start building an actual working text adventure with some interactivity.

And since we’re writing our classes following object oriented principles, like encapsulating data and logic and ensuring there’s little to no duplication of functionality, you’ll notice that we’ll often be working in “abstract” ways with our code (less transparency in what’s being done under the hood within classes), even as our features become more complex. This is a sign and another beneficial symptom of object oriented programming.

With the proper abstraction of objects (ensuring that objects are modular and not interdependent on each other where unnecessary, aka being loosely coupled), we’re able to work on things at a higher level, and lessen the mental strain we would otherwise experience with a growing codebase, since the code’s been modularized and broken down into specialized components.

Now when we make changes, we have specific sections of the code we know to go to, and that other unrelated components won’t change with our modifications because those parts aren’t dependent on each other. Having a well-designed hierarchy of a system of components is another important facet of object-oriented principles.

We know that an Inventory will contain a list of items, and a Player will have an Inventory. Naturally, a Game will have a Player, and also consist of Rooms that the Player can enter. Rooms will contain actions / interactions that the Player can do, which we’ve defined as Prompts.

So, our hierarchy of classes in our system goes like this:

Game
Room
Prompt
Player
Inventory
Display
Input

We clearly see that our Game class will encompass all our other components, and our Room and Player classes should be able to interact with each other directly. Having a sensible hierarchy like that makes it easy for us and any other developers joining a project to continue to make new changes, and extend a codebase with more features. For example, in the future we could make our prompts more robust with sub-prompt types, or extend the functionality of our inventory by creating an Item class (as mentioned in the beginning of this tutorial).

Finalizing our master Game class

It’s time to finalize the creation of our Game class. Game() should be able to be instantiated with properties to populate the game data / content, and also have methods exposed to add or change data as well.

(An important aspect in object oriented programming is ensuring that your code is accessible, or easy to work with, with other code / modules. We’ve gone through all this trouble of abstracting and encapsulating away our game mechanics into nicely contained modules, so we should make sure the API is reasonably designed for usability).

Within Game(), we’ll need properties like $rooms (all the rooms that will populate the game), $startRoom(which room the player starts in), and $endRoom (which room signifies the end of the game, aka the winning room).

We’ll also need functions for the API that will allow us to manage our rooms and other game data, like addRoom() and getRoom(). And a function that will parse the user input and match it against prompt keywords, while handling all the inventory checking and room entering logic. This will be the basis of our game’s AI, so let’s call that function decidePath(). And finally, we’ll also need win() and reset() functions to make our game re-playable.

Examples of creating gameplay and player interactions with more objects

Remember our earlier main.js file? We imported the library we’ve built here at the top like this:

const game = require('./text-rpg-engine');

And since we didn’t have any of our components finished, we weren’t able to add any content to the game to get it working. We can now!

The code below will show you how to use the library we just wrote to programmatically create a new text adventure game.. by using more objects! These new objects will inherit properties from some of our core classes like Prompt and Room.

It’s been heavily commented so you can better follow along with what we’re doing. This will be added to the main.js file, before calling game.init().

// Add a room (by default will be beginning room since it was first added)
const startRoom = game.addRoom('Beginning', 'This is the beginning room');
// Add a second room (by default will be winning room since it was added last)
const endRoom = game.addRoom('SecondRoom', 'You did it! You won!');
// Add required item to room
endRoom.requirements.push('accessKey');

// Add room prompts
startRoom.addPrompt(
'look',
['look room', 'look at room', 'search room', 'examine room', 'look in'],
{
'successText': 'You see a room with a door to the right and a statue in the middle.'
}
);

startRoom.addPrompt(
// name of prompt (required)
'Go rightt',
// keywords that will activate prompt (required)
['go right', 'move right', 'open right', 'enter right', 'door right', 'right door'],
// results of prompt
{
// successful prompt result text (required)
'successText': 'You enter in the access code "14052" and successfully open the door.',
// failed prompt result text (optional)
'failText': 'The door is locked with an access code!',
// room to enter as result of prompt (optional)
'roomToEnter': 'SecondRoom',
// items added to inventory after successful prompt result (optional)
'itemsGiven': 'trophy'
},
// required items to successfully do prompt (optional)
['accessKey']
);

We don’t have to instantiate a room with any prompts, because the API allows us to add new prompts to an existing room (with the addPrompt() method). The results of a prompt is stored in a map containing the keys $successText, $failText, $roomToEnter, and $itemsGiven, and will contain both the success and failure results of a prompt action done. Finally, remember the last (optional) parameter a prompt takes is an array of items that will be the prerequisites for that prompt (if all the required items aren’t there, the prompt will fail).

We only have two rooms, but that is enough for a full game (just two rooms are needed for a start and end). In our starting room, we have one Prompt available, entitled ‘go right’, which will allow the player to enter the second room and win the game. But, that room, as well as the prompt that results in entering the room, requires an item called accessKey!

This is what we’ll see with the above code added to our main.js file, after re-bundling the main.js file with browserify, and opening the game up in our browser. You can see it works, we have user interactions and a working game! Sort of.

We’ll need more prompts of course, to actually have some demonstrable amount of gameplay. And remember, prompts can be anything, and they can result in any type of item, or go to any room, or return any text. Basically, just remember prompts are extremely versatile, because they’re generic objects that can take in data.

Text adventures are notorious for their clever use of puzzle mechanics, so we can be clever with our prompts to provide some uniqueness and variety in the player interactions.

Let’s add three more prompts to our start room, to make it so that the player has to solve a basic “puzzle” before proceeding to the next room and winning the game:

startRoom.addPrompt('get statue', ['get statue', 'pick up statue', 'take statue', 'pick statue'],
{
'successText': `You pick up the statue. It feels heavy in your hands, and there's something hanging off
the bottom.`,
'itemsGiven': ['statue']
}
);

startRoom.addPrompt('rotate statue', ['rotate statue', 'rotate the statue'],
{
'successText': 'You take the note from the bottom of the statue.',
'failText': 'You have no statue to look at!',
'itemsGiven': ['note']
},
['statue']
);

startRoom.addPrompt(
'look',
['look at note', 'examine note', 'take note', 'get note', 'check note', 'read note', 'look note'],
{
'successText': 'You look at the note and find an access code: "14052."',
'failText': 'You have no note to look at!',
'itemsGiven': ['accessKey']
},
['statue', 'note']
);

And the rest of the main.js file will look the same as before:

With our final changes in the example files, run browserify example/main.js -o example/bundle.js, and open index.html again in a browser.

We have a fully-fledged working text adventure! A (short) game from start to finish, with a puzzle—the player must rotate the statue to get the access key (but we hinted towards the solution in the game text, as any fair game would do). All working in just ~400 lines of code.

We’ve gone through the entire creation and build processes (transpiling / bundling) for modern JavaScript web development with ES6 syntax. We’ve gone through the core mechanics of text role-playing games and object-oriented programming in general, and why object oriented design is extremely suitable for the dynamic and procedural nature of interactive fiction.

Now you can take what you’ve learned and expand what you’ve built with further features, classes, and interfaces. You can add graphics via ASCII art or even link images from an assets folder to rooms, that the display would render alongside the text. Or, you can take on our bonus feature challenge, and add a function to the Game class so that it can load in data from a static JSON file, making the components we’ve built accessible in extremely useful new ways.

We hope that you’ve enjoyed this in-depth, practical guide on creating text adventures and ES6 classes in JavaScript with object-oriented principles, and would love to hear your thoughts on the examples, techniques, or anything else in the comments below or on Twitter!