Tutorial: Creating a Text Adventure Game in NodeJS (Part 2)

NicolasBrondinBernard

Author
@NicolasBrondinBernard

We continue our adventure by connecting the Parser from last time to the game engine.

Article published on 10/09/2020, last updated on 10/08/2026

For those who missed the beginning of this tutorial, I invite you to read it right here: https://code-garage.fr/blog/tutorial-create-a-text-adventure-game-part-1-syntax-parser/

Last time we created our basic syntactic parser in order to validate the sentences entered by the player and to split each word so as to classify them.

The goal of today's tutorial will be to make the connection between this parser, the terminal, and the game engine. The "engine" part is very simple for now, as it will be the subject of the next tutorial!

Part 2: Understanding the player

Handling keyboard input

For now our parser only works when passing it sentences written in advance, but for a text-based game, the player must first and foremost be able to write text!

The goal of the project is to end up with sufficiently modular code, so I decided to create a special class to handle the keyboard: the InputManager.

//InputManager.js
const readline = require('readline');

class InputManager {
    constructor(on_command){
        this.on_command = on_command;
    }

    request_input(){
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
        });
        rl.on('line', (input) => {
            rl.close();
            this.on_command(input);
        });
    }

}

module.exports = InputManager;

This class will be instantiated by the game engine, passing it a callback for each new command entered by the player, and since it's the game that decides when to give the floor to the player, we trigger the terminal reading via the request_input() method.

The fact that this class is separate means, for example, that it can be replaced by another class with the same interface but working with voice commands!

The voice version obviously won't be covered by the tutorial!

The command manager

For now we know how to listen to user input, parse it to annotate it and check its syntax, but we need to turn it into concrete actions!

The purpose of the command manager will be to define the role of each token in the player's sentence and to determine which actions should be executed.

In this first version, the "noun" token will be used to select the object to target, and the "verb" token will be used to perform an action on it. For now, other tokens such as prepositions and articles are ignored and simply serve to give the player some flexibility in how they phrase things.

//CommandManager.js

class CommandManager {
    constructor(game_instance){
        this.game = game_instance;
    }

    process_command(tokens){
        let verb = tokens.find(function(token){
            return token.type === 'verb';
        });
        let noun = tokens.find(function(token){
            return token.type === 'noun';
        });

        if(noun){
            this.game.target_item(noun.value);
        }

        if(this.game.check_target()){
            switch(verb.value) {
                case "lire" : {this.game.read(); break;}
                default: {throw Error('Unknown command');}
            }
        }
    }

}

module.exports = CommandManager;

You will have noticed that if the sentence doesn't include a "noun" token, the command is still executed, but this time using the last targeted object, which will allow us, for example, to call the commands "read letter" and then "reread" without having to specify the object each time.

The basic game engine

Here our engine will serve three main purposes:

  • Linking all the previous components together
  • Exposing a very basic representation of the world (here, a list of objects)
  • Starting to write the logic behind the commands
//Game.js

const Parser = require('./Parser'),
InputManager = require('./InputManager'),
CommandManager = require('./CommandManager');

class Game {
    constructor (){
        this.parser = new Parser();
        this.command_manager = new CommandManager(this);
        this.input_manager = new InputManager(this.command_listener);
        this.input_manager.request_input();

        this.target;
        this.world = [
            {name: "lettre", text:"J'ai pris les clés et fermé la porte, si tu veux sortir trouve le double, je ne sais plus où il est !"}
        ];
    }


    command_listener = (line) => {
        try {
            let tokens = this.parser.parseText(line);
            this.command_manager.process_command(tokens);
        } catch(e){
            console.log("Je n'ai pas compris cette commande.");
            this.input_manager.request_input();
        }
    }
}

module.exports = Game;

When the Game is instantiated, we also instantiate the Parser, the InputManager, and the CommandManager. By passing the "command_listener" function as the InputManager's callback, we create the link between the parser and the command_manager, which will process the data one after the other.

//Game.js

    target_item(target_name){
        let target_item = this.world.find(function(item){
            return item.name.toLowerCase() === target_name.toLowerCase();
        })
        if(target_item){
            this.target = target_item;
        } else {
            console.log("Je ne sais pas de quel objet vous parlez.");
        }
    }

    check_target(){
        if(this.target){
            return true;
        } else {
            console.log("Vous devez spécifier un objet");
        }
    }

Next we add the two functions whose purpose is to manage the player's current target. Here we're not talking about "target" in the combat sense of the term, but simply placing a "pointer" on the object the player is looking at at a given moment.

Eventually, the target_item method will be much more complex, since it will need to be able to search the player's inventory as well, and to find objects by other keywords than their exact name. For example, "Mom's letter" should be targetable just by typing "letter" if there is no ambiguity among the possible objects.

//Game.js
    read(){
        if(this.target.text){
            console.log(this.target.text);
        } else {
            console.log("Vous ne pouvez pas lire cet objet");
        }
        this.input_manager.request_input();
    }

And finally we create our first concrete action, which will allow us to read the content of an object, but only if it actually contains text!

In the next part of this tutorial we will come back to enhance our game engine as well as our world, in order to turn this simple list of objects into a real, more complex data schema.

Don't forget that you can always find the complete code presented in this tutorial on the project's Github:


Annie Spratt sur Unsplash

Finished reading this article?
Our complete courses
Take it to the next level with our courses!

Complete courses, exercises and certificates to really learn programming!

4.8 average rating

Comments (0)

to leave a comment

No comments yet