Recently, I had an inspiring discussion with fellow programmers, we were talking about interesting side projects/programs to quickly “try out” new programming language/job interview tasks. One that’s been mentioned was coding a Z-machine interpreter that’s capable of playing Zork I. The Z-machine is a virtual machine developed by Joel Berez and Marc Blank, used for numerous Infocom text adventure games, most notably the Zork series. In all honesty, I’m probably a few years too young so didn’t get to play Zork when it was big (I did play old Sierra adventures back when you actually had to type commands, though, one of the the reasons I started to learn English was Police Quest I. Took me more than 3 months to finish this game). Few weeks later I had a whole weekend to myself and decided to give it a try. As it turned out — it really was a lot of fun. I also gained lots of respect for the Infocom guys, there are some really creative ideas there, especially given space/memory limitations (zork1.dat file I found was ~90k). At first I wanted to do it in Rust (language I wanted to experiment with), but in the end decided to play it safe, limit the number of unknowns and went with Go (my second time). It actually turned out to be a good choice, basic implementation is ~1500 lines of Go and comes with some nice features for free (like cycling through past commands with the up arrow). Went fairly smooth too, stumbled few times, mostly because of me missing some little detail (like call 0 == return false or some off-by-1 mistake when indexing properties). One that took me probably most time was subtle bug in the ‘change parent’ routine that’d cause the game to break apart after I had picked up something. Luckily, I found an easy repro case, if I didn’t pick up a water bottle, I could move the rug fine, otherwise, it’d complain about rug not being there. I didn’t want to spend time writing a fullblown debugger, it was a weekend project after all), so spent some time comparing instruction traces for “good” and “bad” runs, trying to see where they drift apart. Eventually coded a quick diff application (comparing it in notepad was too slow) and found what was going on, it was fairly smooth sailing after that.
The good thing, it’s very easy to start with a basic framework that does nothing but advances IP accordingly and then keep on filling the gaps, adding implementation for required opcode types, opcodes themselves etc. I simply started with NOPs everywhere (with basic implementation calling panic(“NOP”)) and then kept on implementing until finally seeing the “you are standing in an open field of a white house” message. The good thing is, getting to this point requires implementing most of the basic functionality, it’s mostly adding opcodes after that (aka the easy stuff).
Big pieces that are still missing are save/restore, other than that it should be fairly complete (it’s version 3 only).
Useful links, if someone would like to give it a try:
- Documentation: Z-Machine Standards Document, Learning ZIL (original Infocom document, PDF), Description of ZIP (Infocom doc, PDF)
- Ztools (The Infocom Toolkit) -set of Z-machine tools. Very useful for making sure your own interpreter works as it should. I simply disassembled Zork I with TxD and by outputting IP from my own program I could quickly verify what instruction failed (or what should be there). A lifesaver for sure.
- my example implementation in Go. As mentioned – it’s not 100% complete weekend project and might contain bugs.