Yesterday I posted a proof of concept of a simple text-based adventure game built in Vue.js. While it was incredibly simple (and a bit broken, sorry), I made some progress in updating the engine today that I thought would be cool to share. Pretty much nothing visually changed, but I made some structural changes that I think will go a long way to improving the core game.

One of the first things I did was add support for command aliases. I already had this in terms of "w" being an alias for "west", but I wanted a generic structure for this I could update more easily. With that in mind, I added a simple aliases.json file:

{
	"north":"n",
	"west":"w",
	"east":"e",
	"south":"s",
	"look at|l at|l":"look"
}

Take a look at that last line. The use of pipe allows me to have multiple variations of an alias. I could have simply repeated them, but I liked this form. It felt more compact.

Next, I added a file called commands.json. It's only purpose was to serve as a definition of legal commands:

[
	"w",
	"e",
	"n",
	"s",
	"look *"
]

Again, the last line is important here. My game doesn't support "looking" yet, but I used the form look * as a way of supporting the ability to look at anything. Eventually my code will map "look X" to a result that says, "She wants to do command look with an arg X."

I then combined my three data files into one JSON file called data.json and simply fetched that on start. I did this via a utility script I'm going to show in a bit. Here is the new startup routine:

mounted() {
	console.log('Loading room data...');
	fetch('data.json')
	.then(res => res.json())
	.then(res => {
		console.log('Loaded.');
		this.aliases = this.prepareAliases(res.aliases);
		this.commands = res.commands;
		this.rooms = res.rooms;
		this.room = this.rooms[this.initialRoom];
		this.loading = false;
		//nextTick required because line above changes the DOM
		this.$nextTick(() => {
			this.$refs.input.focus();
		});
	});
},

The prepareAliases method simply handles taking those pipes and 'exploding' them out:

prepareAliases(s) {
	/*
	To make it easier for the author, I allow for X|Y, which I'll expand out at X:alias and Y:alias
	*/
	let aliases = {};
	for(let key in s) {
		if(key.indexOf('|') === -1) {
			aliases[key] = s[key];
		} else {
			let parts = key.split('|');
			parts.forEach(p => {
				aliases[p] = s[key];
			});
		}
	}
	return aliases;
},

As the comment says, I want it to be easier for the author, or game developer. And that desire is what drove my final change. As I mentioned, I built a simple utility that would combine my JSON files into one. But I wanted to do something special for rooms. In the first version, rooms were defined in JSON. Here is an example:

"initial":{
	"description":"This is a rather boring room, but despite that, you feel the pull of a new adventure!",
	"exits":[
		{
			"dir":"w",
			"room":"westroom"
		},
		{
			"dir":"e",
			"room":"eastroom"
		}
	]
},

While that isn't bad, the description field doesn't really work well for long descriptions. I can't use line breaks or quotes without escaping and what I really want is the ability to just write. So I came up with a new format:

<description>
You are in a rather simple room. Plain white walls surround you and nothing really
stands out of the ordinary. Despite that, you feel the pull of a new adventure and
steel yourself for what waits ahead!
</description>

<exits>
w|westroom
e|eastroom
</exits>

I've got a simple description tag where I can put in anything I want. Exits are a line-delimited list of values where each line has a direction and destination. Finally, the name of the file is the "key" for the room. So this file would be initial.room.

With that in place, I built this utility:

/*
Simple Node script to read in a few JSON files (soon to be text too) 
and output a JSON output that can be saved into a file.
*/
const fs = require('fs');

//let rooms = fs.readFileSync('./rooms.json', 'UTF-8');
let aliases = fs.readFileSync('./aliases.json', 'UTF-8');
let commands = fs.readFileSync('./commands.json', 'UTF-8');

let data = {};
//data.rooms = JSON.parse(rooms);
data.aliases = JSON.parse(aliases);
data.commands = JSON.parse(commands);
data.rooms = {};

/*
now parse everything in rooms
*/
let rooms = fs.readdirSync('./rooms');
let descRe = /<description>([\s\S]+)<\/description>/m;
let exitRe = /<exits>([\s\S]+)<\/exits>/m;

rooms.forEach(room => {
	let r = {};
	let roomContent = fs.readFileSync('./rooms/'+room,'UTF-8');
	let desc = roomContent.match(descRe);
	r.description = desc[1].trim();

	let exitStr = (roomContent.match(exitRe))[1].trim();
	let exitArr = exitStr.split(/\r\n/);

	r.exits = [];
	exitArr.forEach(e => {
		let [dir,loc] = e.split('|');
		r.exits.push({"dir":dir, "room":loc});
	});

	let name = room.split('.')[0];
	data.rooms[name] = r;
});

console.log(JSON.stringify(data));

And then ran: node util.js > data.json. And voila - done. You can view the code here (https://github.com/cfjedimaster/webdemos/tree/master/vuetextbasedgame) and actually play it here (https://cfjedimaster.github.io/webdemos/vuetextbasedgame/).

Header photo by Jeremy Bishop on Unsplash