You ever play this game in your head, where you have a piece of software you love, and you find your mind idly imagining how to build it yourself?
I want to say this is a quirk of the new programmer, that they learn a little syntax and then imagine they’re going to write the perfect for-loop that reproduces BioShock, but it’s not; it’s a healthy and normal piece of any programmer’s trade, if not their life. And it’s a good intellectual exercise! You don’t even have to write any code; after all, code is just a form of expression. What you really end up thinking about is how you would structure something.
If you had to write an email client, where would you start? Or a word processor? Or Tetris? Are you thinking about how you would lay out the data files, what the basic loop would look like, the libraries it might use, or what sort of framework you would build for it?
As a child, I used to run this thought process with Zork.
Zork continues to be a marvel in my mind; a game that I played constantly as a kid, and didn’t beat until I was thirty-five. It’s a world that’s not as big as you think but massive enough to fill your imagination. It felt like it could be as snarky as it wanted in a world where everything else was just so serious: most of your gaming time in the early 80’s was spent with your nose in an encyclopedia trying to figure out where in the world Carmen Sandiago was hiding, or dying of dysentery on another slog down the Oregon Trail.
But Zork? Zork made you explore an entire underground empire tucked under a little house. You had to fight a troll, raid a tomb, dodge a thief, perform an exorcism, walk across a rainbow, sail down a river, and always—always!—race through the creeping darkness as your lantern slowly burned out. And this was just Zork 1! We aren’t even to the hot air balloon you need to pilot through an active volcano in Zork 2!
All of this was just text written to stdout, reading simple commands from stdin, more or less. Your imagination supplied the graphics, a neat sleight-of-hand that expanded the world to be as massive as you could dream it to be.
Somewhere in a landfill, there are reams of paper filled with BASIC code, of me trying to reimplement Zork. It never took long for it to descend into a mess of conditionals and special cases that collapsed under its own weight. But every now and then I would try again. As a newbie with BASIC it was just a long chain of...
IF $ACTION = “GET LAMP” THEN
...that never made it past the first room, then I learned C and it was at least the vaguest dream of a parser:
if ((strcmp(verb, “get”) == 0) && (strcmp(noun, “lamp”) == 0))
By the time C++ rolled into my life like a boyfriend that convinces you to read Atlas Shrugged, it was suddenly Everything Is An Object and we had...
class CPlayer { public: void getLamp(CObject &lamp); };
...though nothing was actually better at this point.
Do you know how Zork is actually built? It not only uses its own programming language, it uses its own CPU, too.
Infocom had a problem, you see: in the early 1980s, one didn’t just pump out a build of one’s game for the PC and call it a day, because at the time, no one really agreed about what a standard desktop system would be. There were Apple II computers, the TRS-80, janky ancient MS-DOS machines, the Commodore 64, Amiga...Radio Shack had a thing, hell, even Atari sold a personal computer.
None of these systems were compatible. If you wanted to sell a game to the largest audience possible, you were going to be porting it. A lot. And by “porting,” in this era, I mean “writing it completely from scratch each time, probably in assembly language.” Each of these systems were extremely resource-constrained, they all had wildly different feature sets, and cross-platform compilers for high-level languages either weren’t practical or flat out didn’t exist. This produced a million conversations and wild arguments about the “best” version of a game (“Oh sure, the graphics are better in the Amiga version of Out of this World, but they added an extra scene to the DOS version!” etc).
Zork had the luxury of needing the barest of user interfaces: just text sent to the screen in response to text entered on the keyboard. But even here, it bumps into memory limitations on the C-64, and other stupid quirks, like the Apple II not having lowercase letters for crying out loud, before you even consider all the different CPU architectures floating out there in the market.
So to make Zork, Infocom’s plan was to build a game for an imaginary CPU, called the “Z-Machine,” and then their task in porting the game was not to port the game but rather make sure every target had a working Z-Machine emulator. This let them have one code base for the game that worked everywhere, and once the Z-Machine was up and running on a new platform, they got the game for free (and, in terms of what turned out to be a pretty prolific catalog of interactive fiction, each new game could pop up on all the supported platforms pretty much right out of the box). This was an undoubtedly good technical decision on Infocom’s part, not just for their business at the time but because this has allowed their games to pop up in new places, decades after they went out of business.
If you want to play Zork on a Linux system—”Linux” being a word that didn’t exist when Infocom did—you can run a native Linux binary to do so. If you want to play it on your cell phone—cell phones being a thing that didn’t exist when Infocom did!—you can! All you need is an app that implements the Z-Machine.
This branched out to a million other places, probably in competition with Doom for the "ported to most unique platforms" record. Unlike Doom, though, you could play Zork over IRC, by text message, on Twitter, and even through VoIP. All because the Z-Machine was easy to implement so it didn't matter what the game itself did.
The Z-Machine is not super-complicated, partially out of necessity and partially because it didn’t have to be. It’s one of those things, like Pong or Tetris, that every programmer should try to write, just for fun, at some point in their life. The spec is here. You only need to implement 14 instructions to get the Z-Machine to print Zork’s intro text, and this is a great motivator to get you to keep going to finish the project.
Writing a Z-Machine for fun is what got me to write mojozork: a single C file that is just enough of the Z-Machine to complete Zork 1 (and probably several other early Infocom games). Clocking in at around 1800 lines of C, this is a reasonable weekend project that you knock out, nod your head sagely at, and move on with your life.
And I tried. I tried to move on with my life, but there’s this technical detail that fucking ruined my brain.
The Z-Machine treats lots of things as “objects,” but in the physical sense more than the C++ sense. The Zork version of the Z-Machine keeps all its objects in a table that uses an 8-bit index. Object zero is basically the NULL object, so you have enough space in the table for Zork to use 255 objects. And it uses almost all of them! I know, you’re sitting here counting them on your fingers...the lamp, the sword, the leaflet in the mailbox. But lots of things are “objects” in Zork: each room, the player, the thief...some objects exist in different states and are swapped out, so there’s the brass lantern but also a burned-out lantern and a broken lantern, all separate objects, and the last two get swapped into the scene if tragedy befalls the first. Objects follow Newtonian law and are neither created nor destroyed. When you kill the troll or the thief, their objects just get moved out of the room but still exist in the table.
Add everything up, and it’s 250 objects.
Which means the game could hold five more.
And this is when my brain descended into madness.
What would happen if I added more players to the game, to fill these last few object slots in the table, and let several people play the same instance of Zork at the same time? What if each player walked around on their own, and carried their own inventory, but the world itself is affected by all of their actions collectively?
Like everything, it’s not that simple. But it was close enough to be feasible! And one day, in an act of programming bravado, my mojozork project morphed into multizork, and this text-based game from 1980 was suddenly a multiplayer game. I added some new objects to the table and a lot of other tapdancing, and soon enough I had two people standing by that mailbox, west of the house. But there was a lot of tapdancing still to be done.
Turns out that Zork is fairly “object oriented” in that you can slot new objects into the world tree and they can happily coexist with their own set of states, but there’s a lot of global variables in Zork that assume there’s only one player (a not-unreasonable assumption, to be fair). So while each player object has it’s own state for health level and such, things like how many items can be held and whether you’re currently dead (a temporary state in Zork!) are global variables. Not to mention interface settings (like “verbose” and “superbrief”) which assume there’s only one butt in the seat for the game, and each room object having a bit that is set the first time you visit it, which the game uses to decide if it should give you a description of the area or just print its name.
Also, there are places where the game hardcodes “object #4” to mean “the player,” although it also keeps a global variable with this number that it uses most of the time.
These are all solvable problems. I wrote up a little telnet server that lets people connect (because it was financially infeasible to send every player a 300 baud modem to dial in with). Then they start or join a game, and the server feeds them the game like it was usual old Zork. Easy. Mojozork implements each Z-Machine instruction as a function pointer, so I override several of those to make the game multiplayer friendly: it runs until it hits a READ instruction, sends any output from the game to the current player, then it breaks out of the game loop and waits for anyone to type something, and then switches to that player, feeds that input back into the Z-Machine and runs until the next READ. Repeat repeat repeat.
Between each player’s run, we swap out pieces of the Z-Machine’s dynamic memory to accommodate the player (globals that are player-specific, the touchbits of every room in the game, etc). We also take some steps to examine dynamic memory so we can report to other players in the same room when you come and go, and what command you gave.
One discovery was that while there is room in the object table for five more items, in that it can reference 255 objects and only has 250, there is no unused memory in the table. Infocom’s tools pack this area tight; the very next byte after Object #250 is the start of the property data table (which holds arbitrary state for each object), and the very next byte after that are the global variables.
Since we need more objects, and those objects need more property data, this was a bit of a problem. I ended up adding a janky sort of virtual memory to the Z-Machine; the CPU uses 16-bit linear addresses, and when it requests the memory address of a multiplayer object’s property, I report an address all the way at the end of the address space (working back from 0xFFFF). This happens to work because while Zork’s memory image is larger than this, the Z-Machine splits memory into three sections: dynamic memory (game state lives here and the Z-Machine can write to it), static memory after that (things that don’t change live here and the Z-Machine can not write to it, like the dictionary), and high memory at the end (code; the Z-Machine can execute it but can’t read or write it at all). The idea was that on extremely memory-constrained systems, the dynamic part would definitely fit in RAM and the rest could be swapped from disk as necessary. With Zork 1, we’re lucky that we’re into the CPU-inaccessible high memory area before we hit the end of the 16-bit CPU-accessible area, so we can just steal a block at the end of the address space, knowing the game will never be able to read or write it. When we see an instruction try to touch it, we can simply redirect to the appropriate multiplayer data, which is actually living nowhere in the Z-Machine’s address space, but it doesn’t know any better and it all works out. We’re also fortunate that Zork does not try to access the object table directly; the Z-Machine specs say this is legal to do, and some games do it to achieve various effects, but Zork only uses the Z-Machine instructions that handle this for the game, so it was easy to override those instructions when we need to access a specific multiplayer object.
When the game tries to operate on Object #4 (the player), multizork deals with that behind the scenes by choosing the actual current player object. There are six places in the code where Zork compares integers against the number 4 instead dealing with objects, and those get patched in the game’s routines when we switch players (other than that, it’s all original Zork Z-Machine code running without direct changes).
Once all that was in place, the game could stumble along with multiple players all cooperating (or refusing to cooperate, perhaps) to “solve Zork,” which in actual terms means collecting every treasure in the game, placing it in the trophy case, which opens the entrance to the Stone Barrow where there is a bridge to Zork 2.
There are places where this whole concept is janky, and in the spirit of a game that can be merciless about allowing you to make it unwinnable, I decided not to work around them. There are not enough light sources for four players in a universe where darkness equals death, or someone could be holding a crucial object and disconnect, leaving it frozen to their immobile body until they return...if they return. There are possible solutions to fix these concerns, but one could also just make the game unwinnable without warning by eating the garlic seven moves in, so for now I’m hoping this produces the occasional hilarious disaster; for example, the troll might kill the player that has the sword and lamp, scattering him and his belongings in the forest above ground, leaving everyone else in the room defenseless against the troll and unable to flee because of the grues. They’ll be joining him above ground shortly.
My original plan was to make this generic enough to make other Infocom games multiplayer as well, but I think in practice this would be either a lot of work or completely impossible. Not all Infocom games have that Metroid vibe where you can wander where you like, pushing on the edges of the known map until a newly-discovered item opens up a new section. Some games, like Planetfall or Hitchhiker’s Guide to the Galaxy, are actually quite linear in nature, and wouldn’t benefit so much from letting several people wander in parallel. I do think it would be interesting to try this with Suspended, where each player gets to control a different robot, but there would be enormous downtime between players.
But more importantly: there’s an enormous amount of hardcoded tapdancing in multizork to work with this build of Zork 1. It won’t even work with other versions of Zork 1, as the global variables will be in a different order at the least.
So go ahead and give it a try! Telnet to multizork.icculus.org. If you don’t have telnet installed, Linux and Mac users can probably try “nc multizork.icculus.org 23” and Windows people can install PuTTY.
The server saves transcripts from every run on the web, so you can show off your victories and defeats to your friends and enemies. It’s smart enough to let you rejoin a game in progress, archiving inactive games to an sqlite3 database, so even if everyone quits out and comes back next year it’ll pick up where you left off.
The source code, of course, is available. Not just for multizork, but for Zork 1, as the original code, in ZIL (“Zork Implementation Language”) is sitting on GitHub, and it’s both as fascinating to read as it is extremely difficult to understand in these modern, non-LISPy times.
I will be making fixes and improvement based on feedback, but this project is done and my brain could finally settle down, knowing that this bonkers idea was actually achievable. Now to do something more normal, like finally crafting the perfect for-loop to reproduce Doom.