Here’s the goal:
A way to make a webpage that lets you have an interactive dialogue by choosing options, like the old Interactive Fiction games.
The input file is very simple:
PRINT Hello! WAIT ; This is a comment. Wait for a key press or mouse click PRINT I would like to ask you a question. PRINTW Please don't be nervous. ; The 'W' means WAIT afterwards PRINT Are you happy? PRINT [0] Yes PRINT [1] Not really... CALL INPUTINT(0, 1) IF RESULT == 0 PRINTW I'M SO HAPPY THAT YOU'RE HAPPY! ELSE PRINTW Then I am miserable 😦 ENDIF
The challenge is to make a webpage that could read that input, and run it, producing the interactive output shown in the video above.
Perhaps have a think if you don’t know how you would implement this. It is perhaps not so obvious.
Here is the approach that I decided to take. Note that I only wanted to spend a weekend on this, so I took various shortcuts that I wouldn’t recommend if writing for production:
- Read in the input file in, using javascript with a Tokenizer and Parser.
- Output javascript using the above Parser, effectively making a transpiler from the input language to javascript. Call outside javascript functions to ‘PRINT’ etc.
- Use ‘eval’ to run the resulting transpiled code. Yes, eval is evil and this is a case of don’t do what I do.
- Use Promises, via async and await, to ‘pause’ and allow interaction. Implement ‘WAIT’, ‘PRINTW’ ‘INPUTINT’ etc functions in javascript with Promises, so that they only resolve when the user has clicked, typed a number etc.
- Display output by just appending to a list, and displaying that list in React.
1. Read the file in, using Javascript with a Tokenizer and Parser
I used jison. Although the README blurb says that it is for Context Free Grammars, because it is based on BISON which is likewise, it does actually support context. This was vital because the input file language is not actually a context free grammar.
2. Output javascript using the above Parser, effectively making a transpiler from the input language to javascript
The correct way to do this would be to create an Abstract Syntax Tree, however I didn’t want to take too long on this, and instead simply outputted javascript code as a string.
3. Use ‘eval’ to run the resulting transpiled code.
This is very frowned upon, but this was a weekend project, so….
There is one trick that I used here. I wrap the entire program to be ‘eval’ed like:
async function() { ..... }()
This allows the program code inside to use async and await to wait for input, and the eval is returning a Promise. One minor point – when we use eval to evaluate this, we want to catch errors that the Promise throws, to provide clear feedback to the user if there are problems. E.g.
try { await eval(program); } catch(e) { ... }
4. Use Promises, via async and await, to ‘pause’ and allow interaction. Implement ‘WAIT’, ‘PRINTW’ ‘INPUTINT’ etc functions in javascript with Promises, so that they only resolve when the user has clicked, typed a number etc.
I used two layers of callbacks, to implement a poor-man’s publish and subscribe system.
So the transpiler turns:
PRINTW Hello!
into:
printLine("Hello!"); await wait();
And the wait() function is implemented as:
async function wait() { await new Promise( resolve => cb_setClickListener(() => resolve()) ) }
So we subscribe to a click listener via the callback ‘cb_setClickListener’ and then resolve the promise (and thus resume running the program) when the click is published.
Inside the React page, we now listen for clicks and publish it to the callback:
this.state.clickListener && this.state.clickListener() }>
And likewise for keypresses. (Note, I’ve simplified the code here a bit. In the real code, I pass the keypressed etc, so that INPUTINT can listen to a particular key).
5. Display output by just appending to a list, and displaying that list in React.
The ‘printLine’ function was implemented like:
function printLine(str) { const newLine = <div>{str}</div> ; this.setState({displayLines: [...displayLines, newLine]}) }
One extra detail – if the string starts with a number in a bracket like: “[0] Yes”, then I output a link that publishes that number like:
<div> this.state.keyPressedListener && this.state.keyPressedListener(number) } >{str}</div>
This way, when presented with a choice, the user can just click a link instead.  I maintain two separate output lists, so that I can disable the links once we are done with them.
Conclusion and notes
It worked very nicely! I further extended this to support variables, entering strings, showing pictures, and so on.
The language is actually a real language, but very niche and used almost exclusively by Japanese. It hasn’t seen much acceptance or use outside of Japan.