When I began to write NetWhack, I decided that the game would start out as a terminal emulator, and then the game would rely on the terminal emulator side of the software as if it were a game engine. I liked this 'old school' environment because it was nostalgic for me. It is how I learned to code, way back in the day, so I thought it would be a fun little project to write a interface that would look like a Commodore or a PC or some kind of monochrome terminal.
The main problem I encountered was that modern, event driven systems like Android, Javascript and iOS don't allow you to do blocking string input. Trying to do getkey() or input() is impossible. Eventually I found a way to simulate it within the environment of the terminal simulator by sending the line that the cursor was on to a “command processor”, which would interpret the line like a command. This was close to a shell running on an OS, but there were no clear definitions of what was the OS, what was the shell, what was the hardware. Then I wrote an actual input() function that allowed me to prompt the user and set it to a keyword which could be evaluated by the game engine later:
input("SET_NAME", "What is your name? ");
When the user pressed ENTER, a console command “SET_NAME <value>” would be sent to the event queue (the game engine) for processing like a console command.
I then began experimenting with a kind of in-terminal scripting, like BASIC. The concept was simple, I would see if the first part of the string sent to the command processor was a number. If it was, it sent it to an array indexed by that number. This became a program listing which I could “RUN” with an interpreter.
This would finally solve the context problem; normally you cnanot store and use the value of the input locally ex. “a = input()”, because JavaScript (as does LibGDX, PyGame, and many others) runs its game engine code on the UI thread. So every time I accepted string input I would have to lose program context, then respond to the SET_NAME event later on. This was simulated string input, but not simulated blocking string input.
This is really the best you can do. If you try and expand the scripting aspect you will eventually find yourself rewriting the entire game in the virtual scripting language. It might not be worth it, if you can break up your code just a little. With that in mind I set out to make V3 the best damn terminal simulator it could be, but with a structure that would allow someone to turn it into a state machine very easily, if desired.
gameState was removed and flags were added to control the terminal. As a result, we can run the event queue continuously.
For example, Terminal mode (when the terminal is reset):
Now the question is over the queue. Do we use one monolithic atomic queue or try to use multiple queues?
If all events are atomic they are effectively on the same queue so a single queue for all events would be used. Like a message queue. This has a lot of benefits, primarily keeping things simple. The big downside is that there is a massive jump table required, but this can be partially solved by using keywords for various processing modes. such as an ISC command which pulls opcodes, an ENTER command which deals with the user pressing ENTER in terminal “shell” mode, a LINE command which causes program lines to be put into the program listing array, and so forth. These could be separate functions in separate files to keep things neat and tidy.
Multiple queues won't work. If you need a value set it will be set by the host (in JavaScript). If you need to process an opcode as a subroutine of other opcodes (ex. ADD is a construct of other opcodes and runs on the CPU and not in JavaScript) you can just replace AND by inserting the replacement code directly. If you need to set a value immediately that can be inserted before the next command, too. Multiple queues will be a distraction and are unlikely to speed up emulation in a single threaded environment.
Note that if we are in a multi-threaded environment we can always have the game engine running in an off-UI thread. Or the implementation can launch copies in their own threads vs, manual task and context swithching which you would need to simulate in an (ex. JavaScript) implementation. But it would be transparent to the code written for the virtual machine.
A monolithic atomic queue is like a CPU. Since we don't have a ROM, we write high level commands in JavaScript. Slowly, we can implement opcodes and start working more with the state machine.
10 PRINT A 20 GOTO 40 30 PRINT B 40 PRINT C 50 PRINT D LIST RUN A C D
Instead of refactoring into a state machine, I threw together a quick BASIC class which processed commands in a program[] array. It handles print, and goto. Since it is a quick design it can not handle infinite loops or very long programs, since there is no time for the game loop to process updates and render or for the UI to make those changes to canvas.
However, for the point of theory, yes, it is possible to write a scripting language (or a CPU simulator) and have it run in the terminal.
// Define functions function function1() { console.log("Function 1"); } function function2() { console.log("Function 2"); } function function3() { console.log("Function 3"); } // Create a jump table const jumpTable = { "func1": function1, "func2": function2, "func3": function3 }; // Example of calling a function dynamically based on a string parameter function callFunctionByName(name) { const selectedFunction = jumpTable[name]; if (selectedFunction && typeof selectedFunction === 'function') { selectedFunction(); } else { console.error("Function not found"); } }
This example is how to support a large number of opcodes/commands quickly.
V3 is V2 but with changes made in how terminal is controlled by the main program. The code has also become somewhat spaghetti. I partially addressed this by removing the Tile class from V3 and adding BASIC command processing into its own class. But much more work needs to be done on this. The BASIC class should maintain the copy of the program code, and the Terminal class might need to offload it's draw funcion to a Screen class. We might even move event checking into an OS or CPU class. Or both. But the OS class would eventually be replaced by programs running on the CPU (a ROM).
In a real VM, the code would not be stored in a program[] array but in memory[] and read from there by the interpreter. There are lots of little design clashes between the idea of a VM, the idea of a Game (NetWhack) and the idea of s hybrid scripting language written in Javascript. They are not really compatable. So for a V4 the goal will be to pare down and really isolate the functionality of Terminal, and to clean up main by making a class Screen, and also perhaps a class Keyboard, class Mouse, etc.
Well, there are still a few 'bugs' but maybe they are just quality of life fixes. I'd like the terminal to retain it's cursor y position, if possible, during a resize – or, to clear the screen of existing text. Frankly it isn't important enough for me to worry about right now, and it probably isn't really a true bug, so I will leave it for now.
Today's computers are many millions of times faster than (ex. a C64). Even a C65 running at 3.5mhz can 'software' simulate a C64. People have written Sega Master System (Z80) system simulators on 386 quality systems. The point is that today's processors are in many cases over a million times faster than a C64 or 8086 style processor and are overkill for this. Therefore if we move in the direction of a full cpu and system simulator the main problem will not be processing speed, but writing the software. We don't have a ROM/BIOS and we don't have an OS, and those two are both rabbitholes greater than or equal to a 'JavaScript NetWhack'. It's just a little too big to deal with before we write the game.
V4 will be whatever we do to support NetWhack, and then a pared down version will be presented as a V5 “final”. It will represent a programmable terminal emulator slash game engine for character mode programs. Future goals would be for some interactive fiction games, or other interesting games, and possibly, as a platform for teaching JavaScript in a familiar environment (using the terminal as a codebase, and running everything in a Game class. Like modern Android design for example.) For now though, V3 is a sort of stop-gap kludge, and the rewrite will be coming after JavaScript Netwhack.