2
\$\begingroup\$

I'm working on making a text adventure using Electron. I have the basics of the engine working, you can walk around an infinite map. Maps contain things, and you can enter a few basic commands. I'm wondering if the fundamentals of the game (what I currently have) can be improved at all before I go through adding the game's content.

The code currently supports these commands:

  1. look/look around - Prints your location and the contents of the current room
  2. look at - Prints an error (what do you want to look at)
  3. look at [thing] - Prints whether the thing exists where you currently are
  4. north/east/south/west - Moves the player

I'm leaving out index.js (the script that starts electron) and the CSS, since it doesn't seem relevant.

index.html

<!DOCTYPE html>
<html lang="en-US">
<head>
    <title>ProceduralTA</title>
    <link rel="stylesheet" href="css/styles.css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

<div id="log"></div>
<div id="input-prefix">&gt</div>
<div id="input-wrapper">
    <form onsubmit="newInput(); return false">
        <label>
            <input id="input-box" type="text" placeholder="You can type here!" autocomplete="off">
        </label>
    </form>
</div>

<script src="js/game.js"></script>

<script src="js/input-handler.js"></script>
<script src="js/map.js"></script>
<script src="js/player.js"></script>
<script src="js/room.js"></script>

</body>
</html>

game.js

'use strict';

const {ipcRenderer} = window.require('electron');

window.$ = window.require('jquery');

const inputBox = $('#input-box');
const log = $('#log');

const charDelay = 10;

const titleMessage = 'Welcome to ProceduralTA!';
const introMessage = 'You are in a room.';

const gameData = {};

async function logMessage(message, type) {
    const entry = $('<div></div>').addClass(type).appendTo(log);
    for (const char of message) {
        if (char === '\n')
            entry.append('<br>');
        else
            entry.append(char);
        await new Promise(resolve => setTimeout(resolve, charDelay));
    }
}

$(() => {
    gameData.map = new GameMap();
    gameData.player = new Player();
    logMessage(titleMessage, 'msg-system').then();
    logMessage(introMessage, 'msg-game').then();
});

input-handler.js

'use strict';

let input;
let inputArray;

$(() => {
    inputBox.trigger('focus');
});

$(window).keypress(() => {
    inputBox.trigger('focus');
});

function newInput() {
    input = inputBox.val();
    inputArray = input.toLowerCase().split(' ');

    inputArray = inputArray.filter(word => word !== 'a');
    inputArray = inputArray.filter(word => word !== 'an');
    inputArray = inputArray.filter(word => word !== 'the');

    inputBox.val('');

    logMessage('> ' + input, 'msg-player').then();
    logMessage(matchInput(), 'msg-game').then();

    ipcRenderer.send('saveGame', JSON.parse(JSON.stringify(gameData)));
}

function matchInput() {
    const cMap = gameData.map;
    const cPlayer = gameData.player;

    let response = 'Unknown command.'

    if (nextWord('north')) {
        cPlayer.y++;
        response = 'You move north.';
    }
    if (nextWord('south')) {
        cPlayer.y--;
        response = 'You move south.';
    }
    if (nextWord('east')) {
        cPlayer.x++;
        response = 'You move east.';
    }
    if (nextWord('west')) {
        cPlayer.x--;
        response = 'You move west.';
    }

    if (!cMap[cPlayer.y]) {
        cMap.addRoom(cPlayer.y, cPlayer.x);
    } else if (!cMap[cPlayer.y][cPlayer.x]) {
        cMap.addRoom(cPlayer.y, cPlayer.x);
    }

    const cRoom = gameData.map[gameData.player.y][gameData.player.x];

    if (nextWord('look')) {

        if (nextWord('around') || !wordsLeft())
            return 'You are at [' + cPlayer.x + ', ' + cPlayer.y + '].\n' +
                'Contents of this room: ' + cRoom.printMonsters();

        if (nextWord('at') && !wordsLeft())
            return 'What do you want to look at?';

        const object = cRoom.printObject(wordsLeft());
        if (object)
            return object;
        else
            return 'That doesn\'t exist here.';
    }

    const object = cRoom.printObject(wordsLeft());
    if (object)
        return object;

    gameData.map = cMap;
    gameData.player = cPlayer;

    return response;
}

function nextWord(match) {
    const word = inputArray[0];
    if (word === match)
        inputArray.shift();
    return word === match;
}

function wordsLeft() {
    return inputArray.join(' ');
}

map.js

'use strict';

function GameMap() {
    this.addRoom = (y, x) => {
        if (!this[y]) {
            this[y] = {};
        }
        if (!this[y][x]) {
            this[y][x] = new Room();
            this[y][x].addMonster('Zombie', 10);
            this[y][x].addMonster('Skeleton', 5);
        }
    }

    this.addRoom(0, 0);
}

room.js

'use strict';

function Room() {
    this.monsters = [];

    this.addMonster = (name, hp) => {
        this.monsters.push({
            name: name,
            hp: hp
        });
    }

    this.printMonsters = () => {
        let monsterDescriptions = '';
        for (const [index, monster] of this.monsters.entries()) {
            monsterDescriptions += `\n${(index + 1)}) ${monster.name} - ${monster.hp} HP`;
        }
        return monsterDescriptions;
    }

    this.printObject = name => {
        for (const monster of this.monsters) {
            if (monster.name.toLowerCase() === name) {
                return 'You are looking at: ' + monster.name;
            }
        }
        return false;
    }
}

player.js

'use strict';

function Player() {
    this.y = 0;
    this.x = 0;
}
```
\$\endgroup\$
2
+150
\$\begingroup\$

I like this question.

Validator stuff

  • You probably meant &gt; instead of &gt
  • You should consider adding a charset meta tag like <meta charset="UTF-8">
  • You should wire newInput(); return false in JavaScript, HTML is not a good home for JS
  • You are missing a number of semi colons in room.js
  • Not strictly validator issues, but using a form for this seems wrong, I would advocate a key press listener on the input box and altogether throw away the form related code

Possible problems

  • I might be missing something but it seems that you might mingle the title and intro message, why not logMessage(titleMessage, 'msg-system').then(logMessage(introMessage, 'msg-game'));

Major Design things

  • Avoid globals, or at least keep them to a minimum, the reference to the inputfield, the actual input, the array with the word list, all those things have no place in the global scope
  • Read up on MVC, this kind of project would greatly benefit from it

Minor Design things

  • Map.js should not need to know the hitpoints of a monster, I would capture this in something like bestiary.js

  • I would let addMonster return this so that you can chain like 'room.addMonster('Zombie').addMonster('Skeleton')

  • In fact, I am torn on whether the map should know what monster are there, or the room. I am guessing if one day you go with map themes, it should be the map.

  • this.printObject can either return a string or a boolean (false), it should probably always return strings

  • You should not need this;

    gameData.map = cMap;
    gameData.player = cPlayer;
    

    cMap can be considered a 'pointer'

Naming

  • printMonsters does not print, describeMonsters might be more appropriate
  • matchInput does way more than matching input (this is also a design problem)
  • I am not super excited about the c in cMap, it's not very JS like

Array functions

  • Consider using this.monsters.find in printObject, it would be more elegant and equally readable

  • printMonsters could be done with a .map() building strings and then a .join('\n')

  • You can filter out words in a shorter and more readable way like this;

      wordList.filter(word => !['a','an','the'].includes(word));
    
  • Direction processing uses very similar code, I tend to group the data in an array with the related data. This becomes even more important once you start supporting non-cardinal directions like northwest or even up/down.

Not right now

  • At a later stage you want to encapsulate all the UI stuff with jQUery in like ui.js

On the whole, this looks well enough structured. This kind of project sometimes lays dormant for months and you don't want to rack your brain when you pick it up again.

A possible counter-example; (Looking forward to see your version 2 with more features btw)

'use strict';

const data = {
  titleMessage: 'Welcome to ProceduralTA!',
  introMessage: 'You are in a room.'
};

const ui = new (function UI(){
  const inputBox = $('#input-box');
  const log = $('#log');
  const charDelay = 10;
  
  this.logMessage = async function logMessage(message, type) {
    const entry = $('<div></div>').addClass(type).appendTo(log);
    for (const char of message) {
        if (char === '\n')
            entry.append('<br>');
        else
            entry.append(char);
        await new Promise(resolve => setTimeout(resolve, charDelay));
    }
  }
  
  this.focusOnBox = function focusOnBox(){
    inputBox.trigger('focus');
  }
  
  this.clearBox = function clearBox(){
    inputBox.val('');
  }
  
  this.getBoxValue = function getBoxValue(){
    return inputBox.val();
  }
  
  $(window).keypress((e) => {
    ui.focusOnBox();
    if(e.which == 13){
      processNewInput();
    }
  });
  
})();

$(() => {
    data.map = new GameMap();
    data.player = new Player();
    ui.logMessage(data.titleMessage, 'msg-system').then(ui.logMessage(data.introMessage, 'msg-game'));
    ui.focusOnBox();
});

function processNewInput() {
    const input = ui.getBoxValue();
    let wordList = input.toLowerCase().split(' ');
    wordList = wordList.filter(word => !['a','an','the'].includes(word));
    ui.clearBox();
    ui.logMessage('> ' + input, 'msg-player').then(ui.logMessage(matchInput(input, wordList), 'msg-game'));

    //ipcRenderer.send('saveGame', JSON.parse(JSON.stringify(data)));
}

function matchInput(input, wordList) {
    const map = data.map;
    const player = data.player;
    const movementsMap = {
      north: {response: 'You move north', vector: {y:1, x:0}},
      south: {response: 'You move south', vector: {y:-1, x:0}},
      east: {response: 'You move east', vector: {y:0, x:1}},
      west: {response: 'You move west', vector: {y:0, x:-1}},
    };
    //Default response
    let response = 'Unknown command.'
    const word = wordList.shift();

    //Is the player trying to move?
    if(movementsMap[word]){
      const movement = movementsMap[word];
      response = movement.response;
      player.x += movement.vector.x;
      player.y += movement.vector.y;
      //We only need to check this when we move around
      if( !map[player.y] || !map[player.y][player.x]) {
        map.addRoom(player.y, player.x);
      }      
    }

    const room = map[player.y][player.x];

    if (word == 'look') {

        if (nextWord(wordList, 'around') || !wordsLeft(wordList))
            return 'You are at [' + player.x + ', ' + player.y + '].\n' +
                'Contents of this room: ' + room.printMonsters();

        if (nextWord(wordList, 'at') && !wordsLeft(wordList))
            return 'What do you want to look at?';

        return room.printObject(wordsLeft(wordList)) || 'That doesn\'t exist here.';
    }

    const object = room.printObject(wordsLeft(wordList));
    return object || response;
}

function nextWord(wordList, match) {
    const word = wordList[0];
    if (word === match)
        wordList.shift();
    return word === match;
}

function wordsLeft(wordList) {
    return wordList.join(' ');
}

function GameMap() {
    this.addRoom = (y, x) => {
        if (!this[y]) {
            this[y] = {};
        }
        if (!this[y][x]) {
            this[y][x] = new Room();
            this[y][x].addMonster('Zombie', 10);
            this[y][x].addMonster('Skeleton', 5);
        }
    }

    this.addRoom(0, 0);
}

function Room() {
    this.monsters = [];

    this.addMonster = (name, hp) => {
        this.monsters.push({
            name: name,
            hp: hp
        });
    }

    this.printMonsters = () => {
        let monsterDescriptions = '';
        for (const [index, monster] of this.monsters.entries()) {
            monsterDescriptions += `\n${(index + 1)}) ${monster.name} - ${monster.hp} HP`;
        }
        return monsterDescriptions;
    }

    this.printObject = name => {
        for (const monster of this.monsters) {
            if (monster.name.toLowerCase() === name) {
                return 'You are looking at: ' + monster.name;
            }
        }
        return false;
    }
}

function Player() {
    this.y = 0;
    this.x = 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
<!DOCTYPE html>
<html lang="en-US">
<head>
    <title>ProceduralTA</title>
    <link rel="stylesheet" href="css/styles.css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

<div id="log"></div>
<span id="input-wrapper">
  <label>&gt;</label>
  <input id="input-box" type="text" placeholder="You can type here!" autocomplete="off">
</span>


<script src="js/game.js"></script>

<script src="js/input-handler.js"></script>
<script src="js/map.js"></script>
<script src="js/player.js"></script>
<script src="js/room.js"></script>

</body>
</html>

\$\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.