5
\$\begingroup\$

I've been working on a a text based game with a very simple battling system. There are way to many questions I can ask to get reviewed about this project, but I'll take small steps at a time (this is my first post).

I'm mostly concerned about how I handle command input and the execution of said commands within the game. Just to give a simple example of what a command might look like:

go north

go - command (called action in the code, as Command is the whole class)
north (and anything after that) - arguments, you most likely know about these

INPUT

Currently, I've made it work using the readline package from Node. The input is being sent to a "commander" where it is parsed, made into a Command which will in the end be passed to an "executor" which will execute the command. Here's the code:

//Player.ts
import readline from 'readline';
//other imports...

class Player extends Entity {
    // fields, constructor and other methods...

    async getInput(questionPrompt?: string): Promise<string> {
        return new Promise((resolve, _) => {
            const prompt = readline.createInterface({
                input: process.stdin,
                output: process.stdout,
            });

            const question = (questionPrompt) ? questionPrompt : ">";

            prompt.question(question, (answer) => {
                prompt.close()

                resolve(answer);
            })
        })
    }
}

export default Player;
//commander.ts (technically it's inside commander/index.ts)
import Command from '../models/Command';
import Player from '../models/Player';

const actions = ['go', 'look', 'take', 'equip', 'inventory', 'help'];

export const parse = (text: string): (Command | null) => {
    const action = text.split(' ')[0];
    const args = text.split(' ').slice(1);

    if (!actions.find((a) => action === a))
        return null;

    return new Command(action, args);
}

export const execute = (command: Command, receiver: Player) => {
    switch (command.action) {
        case 'go':
            const success = receiver.move(command.args[0]);
            if (success) {
                return console.log(`You have entered a new room.\n${receiver.currentRoom.getDescription()}`);
            } else {
                return console.log(`You can't go there. Please try another one.`);
            }
        case 'look':
            const text = `You are in ${receiver.currentRoom.name}.\n` +
                `${receiver.currentRoom.description}`;
            return console.log(text);
        case 'take':
            const item = receiver.currentRoom.takeItem(command.args[0]);
            if (item)
                return receiver.takeItem(item);
            else
                return console.log(`A ${command.args[0]} couldn't be found.`);
        case 'equip':
            return receiver.inventory.map((i) => {
                if (i.name === command.args[0]) {
                    receiver.equip(i.id);
                }
            })
        case 'inventory':
            return console.log(receiver.inventory);
        case 'help':
            return console.log("No help for now... Maybe later?");
        default:
            console.log('Command error.');
            return console.log(command);
    }
}

export default {
    parse,
    execute,
}

I'm not entirely sure if this is an appropriate way to handle this use case, but this is all I could come up with. The other solution I managed to think of was to have the different commands in a separate folder where I could have them as modules and I could try to import them. This way they can maybe be classes and have their own help messages which will also make it easier to document the commands. The reason I decided to stick with this approach is because it seems a bit easier, and having the commands as separate modules kinds seems overkill to me, maybe I'm totally wrong.

I know I've probably missed something very important, so a link to the whole project can be found here.

\$\endgroup\$
2
\$\begingroup\$

From my experiences in MUDs and text games, I think you would do well to create a class/module that handles each command then inject them into your processor (even manually). It will help a lot with the maintainability but also isolating commands.

import {registerLook} from "./commands/look";
import {registerGo} from "./commands/go";

// Build up a hash of commands. With a MUD style, you would register the shorten
// versions also (so look registers as "l", "lo", "loo", and "look") but removes
// entries that would have a conflict (so "left" would remove the "l" for look but
// register "le", "lef", and "left".
let commands = {};

registerLook(commands);
registerGo(commands);

// Then when you are running the command.
if (command.action in commands
{
  commands[command.action].run(command, receiver);
}
else
{
  // Show error of missing command
}

You could go further with the SOLID implementation by having the commands injected from the top-level, but the main reason for doing this is to let you have the complex logic for the commands moved into a separate file that does one thing only (Single Responsibility Principle) while treating the switch of the commands against an interface (functions inserted into the hash by a register).

Ideally, having a DI figure out the commands would be nice, but I haven't found a good DI system for Typescript I like. (You could also scan the commands directory and inject the commands that way.)

It also lets you nest the commands if you need to. So if you have look at and look into, you could have commands/look.ts break apart the first word and then pass it into look commands (commands/look/at.ts) to further isolate it. Or whatever makes sense.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.