Implementation Notes - ThePix/QuestJS GitHub Wiki

Performance

There is a compromise between speed and convenience. Convenience here is how easy it is for authors to create games, and includes how simple it is to create a basic item, how easy it is to customise to do the bizarre and how easy it is to understand the author's code.

It would be faster to load all the JavaScript files at the same time, however, they would then run more-or-less in size order, with the smallest file going first. That will make it impossible for one file to reliably access something in another file - the most obvious example of that is using createItem in your data files needs the file where createItem is defined already loaded. Now there are ways around this - just delay when createItem is called - but this adds extra complications to the data files and, rightly or wrongly, I chose not to do so.

The second issue is with layout reflow. The browser does a lot of work trying to decide a page layout, and if you then change the layout part way through, it has to start again. I am not sure how big an issue this is with QuestJS, but again I took the decision that convenience to authors was more important. Testing on my PC indicates this typically takes 50 ms which to my mind is perfectly reasonable.

With regards to commands, the slow bits are searching for the best fit command, which takes 15 to 20 ms on my PC, and running world.endTurn which takes a similar time. This means commands are being completed within 50 ms, which is fine.

Initialisation

Initialisation involves loading the right files in the right order. This is complicated by the fact that the files can be in different places. The game (i.e., those usually fond in the "game" folder, the ones specific to a particular game) files can be in a different directory to allow multiple games to be written at the same time. However, the editor also has its own ideas about where files are, the game files being created on-the-fly, while others are static files in a very different location. The index.html file therefore has a script section that sets up some global variables that define where these things are, while the editor can create its own version with its own values.

The initialisation process is kicked off in index.html, which loads lib/_util.js, lib/_settings.js and then your game settings.js file. Once the last has loaded, settings.writeScript is called.

The settings.writeScript file loads all the CSS files, then sets the favicon, and then queues each script file, using settings.loadScript. When a script file has been loaded, it is run, so items and locations are created at this point. As each is done, settings.scriptOnLoad is called, which either starts the next script loading, or, if the queue is now empty, kick-starts the game, first calling world.init then io.init.

The world.init function does some basic housekeeping, including initialising all the world objects, finding the player object and initialising all the commands.

The io.init function sets up the user interface. It creates the side panes, adds event listeners to capture certain keypresses, loads sound files. Then it removes the game loading message, and displays the title. If the game has a starting dialog, that is now displayed, otherwise world.begin is called (if there is a dialog, world.begin will be called when that is closed).

The world.begin function shows the introductory text, settings.intro and runs setting.setup, then calls world.enterRoom which is the function that is called every time the player enters a room, so calls "afterEnter" and "afterFrstEnter".

A Turn

A turn is initiated by the player typing something and clicking enter.

The text is passed to the parser, which will attempt to match the text to a command. If it is successful, the command's "script" attribute is run. Details here. The parser calls world.endTurn to kick off the end-of-turn housekeeping.

  • Change listeners are handled
  • Game turn and time are incremented*
  • "endTurn" functions in objects are run*
  • "endTurn" functions in modules are run*
  • Change listeners are handled again*
  • The "pause" state for any items is reset*
  • Scope for items is updated by world.update*
  • Game state is saved (for UNDO)*
  • The UI is updated

Items marked * occur only if the command returned world.SUCCESS.

Data Structures

This could also be called namespaces; it is kind of the same thing in JavaScript.

Note that only w will be saved (and so game indirectly); nothing else gets saved.

w

The world is held in a dictionary called w. This means you can access any item by prefixing its name with "w." and can access an item by a variable with square brackets.

// You can access the object called "hatstand" two ways:
name = "hatstand";
w.hatstand;
name = hatstand
w[name];

The advantage of using a dictionary is there is no chance of a name collision. The w stands for "world", and is deliberately short.

game

Some transitory game values are stored in a dictionary called game for convenience. It should be reset whenever the player changes rooms with game.update(), and when the lighting changes and possibly other times too.

game.turnCount
game.elapsedTime
game.elapsedRealTime
game.dark

The game object is in w so string, integer and Boolean attributes will get saved.

commands

An array of all the commands in the game. Do not expect this to get saved.

cmdRules

Built-in rules for commands.

parser

Functions for the parser.

io

Hidden functions and variables for I/O.

tp

The text processor.

world

For the world model.

lang

Language specific settings.

test

For unit testing

No data files

A lot of IF systems, including Quest 5, have software files and data files as two distinct things. The data files contain all the information for a specific game, all the details for each item and location and probably more besides. The software loads it in and runs the game. Why not do that in Quest 6?

The problem arises with scripts. People will inevitably want to do something new, so absolutely need the ability to code or to write their own scripts, and that has to be in the data files. To then run code from a data file, you either need to run JavaScript, using eval (or some variant), which has significant security issues, or you need some kind of interpreter in the software, which is the approach Quest 5 takes (and I guess other systems).

It seemed better all around to have the data in JavaScript too. Scripts can be added very easily because it is already code. You have the full power of JavaScript immediately available, with no effort from me. And the data is easier to write too, than say XML (which Quest 5 uses) or JSON.

No Named Capture Groups

Quest 5 uses named capture groups. JavaScript supports named capture groups. So why does Quest 6 not use them?

Simply because they are not universally implemented across all browser. This table indicates the version and date that various browsers introduced them:

Browser version date
Edge 79 15/Jan/20
Firefox 78 30/Jun/20
Chrome 64 24/Jan/18
Safari 11.1 29Mar/18
Opera 51 7/Feb/18
Android browser 81 21/Apr/20
Chrome for Android* 87 17/Nov/20

Data from here

The first upload of Quest 6 to Github was 2/Dec/18. Edge and Chrome had named capture groups for eleven months by then, but it would be some time before Firefix would. And who knows how many users had updated their browser?

Even as I write this (08/Dec/20), only 91% of users have browsers that can use this feature, so even now I would be reluctant to use it. In two years time I expect the situation will be different, but I have to go with what it is right now.