Interactive Fiction in Swift!

A few years ago I got into the Call of Cthulhu roleplaying game and discovered their single-player books, like Alone Against the Dark and Alone Against the Frost. I rather thought that putting together an app version with all the rules built in would be a fun project, but never had the time.

Recently I’ve been investigating Claude Code, and decided this would be something fun to try: a simple rules engine & character sheet for CoC, and a basic choose-your-own-adventure library that I could hook up to it. I’ve been playing with those—and will provide an update here soon—but I got sidetracked. If you know me, you know just how inevitable that is.

This particular sidetrack can be laid at the feet of the inimitable Bill Bumgarner at Apple. When I mentioned the CYOA library I was working on, he reminded me about the old Infocom games, and I fell into the rabbit hole of the ZIL language and the ZIP virtual machine, and this immediately ignited my love of compilers, parsers, and binary file formats. Add in games and, well, I’m a goner.

The old Infocom games were built on a virtual machine called the Z-Machine. This enabled them to compile a game once and write an interpreter program for each target system. Back in the late 70’s and early 80’s this was very important—there were a lot of different computers to support in those days.

The games themselves were written in ZIL (the Zork Implementation Language), itself based on MDL (pronounced “muddle”), itself a version of Lisp. ZIL was created and used by Infocom to develop their award-winning games, and they had tools to work with it:

  • ZILCH: ZIL Compiler Hack. The compiler that turned ZIL code into assembly ready for the next stage:
  • ZAP: Z-Machine Assembler Program. This took assembly code (the format was itself called ZAP) and compiled it into a story file.
  • ZIP: Z-Machine Interpreter Program. This loaded the compiled story file and implemented the game interface.

These, and all the various games written for the system, are well documented at The Obsessively Complete Infocom Catalog by Andrew Plotkin. Part of that catalog includes a large list of interpreters, along with info on ZIL and ZAP. He also hosts the source code for basically all the old Infocom games, similarly to the GitHub repo created by Jason Scott, with some extra pieces.

Ultimately this provided a lot of useful information to feed into Claude to see what I could drum up. Specifically I wanted a library for all ZIL/ZAP/ZIP operations, along with a command-line tool that can run the compile, assemble, and run operations. This is now expanding to an auto-play command and an interactive debugger.

Semi-Vibe Coding

My aim has been to test the water somewhat of AI coding tools’ capabilities. So I set myself an aim of not writing any code myself, but acting as code reviewer and tester. I’ve used Claude Code for this exclusively, and feeding it the available documentation on ZIL, ZAP, and ZIP resulted in Claude being very confident in what it could create. Its confidence, while not entirely unfounded, was perhaps a little too high for the work it put together, as you’ll see.

One thing I definitely found useful was the use of Planning Mode. In this mode, Claude won’t attempt to make any changes to anything, but will instead create a full plan of action and present it to you for verification. Using this mode ahead of any complex task was pretty much essential, to ensure that everything went in the right direction, and helped a lot with setting up guardrails and verifying that they were indeed in place.

The Bad

So Claude (and probably Codex too) has this thing where it’ll decide that “for now” it’ll put in a very basic stub implementation of something important, and then it’ll consider it complete because there’s code in that condition. I’ve run into this all through the project, where Claude will claim to be complete, but I’ll run and things aren’t working. I’ll check the code and there, in the middle, is a TODO or an “I’ll do it this way for now.” Sigh.

Another thing it often does is it’ll make code changes, announce that it’ll build/test/run, and then forget to actually do that & go straight to a long description of everything that’s now in place. Meanwhile, the project doesn’t even compile. This happens relatively frequently, as it turns out.

One more gripe: running out of context space is painful. Whenever it compacts the conversation, it forgets completely what the command-line tool is called (it always tries to run zil-interpreter, when the command is zil run). It’ll also frequently forget the details of longer-term plans, such as the results of a comprehensive review of spec compliance. I’ve taken to asking it to write these things out as files to refer back to and update, just so all that information isn’t lost because it decided to load an entire 2000-line file several times & had to dump most of its context.

The Good

That’s about it for the downside though. It’s generally providing fairly clean code, and it understands enough about parsing, lexing, ASTs, and so on that it was able to pull together the initial project very quickly. It would be able to provide detailed and cogent explanations of complex issues at both the planning phase and while debugging issues.

Speaking of debugging, I was frequently able to work through issues by just asking Claude to look at the code and determine why something was happening (or not happening). It sometimes took a few tries, and there were occasions where it might fixate on something unhelpful, but I was always able to get it back on track. So far there have only been about three cases where I’ve needed to look at the code myself to figure something out, which I think is pretty good. Acting as an engineering manager, this is about as much hands-on work as I’d consider appropriate for me to take on an engineer’s work.

One thing I found exceptionally useful was to create an agent that understood all the information about ZIL/ZAP/ZIP. It was designed & prompted with the details of all the various sources of information (most of which I downloaded locally) and would serve as an advisor to the main Claude process. I needed to do a little fine-tuning here and there, because it often attempted to take on the coding tasks itself, and that wasn’t very helpful for me. In particular, you don’t actually see any output from an agent, you’ll only see requests to make edits, so it’s difficult to understand the context. In the end, I had to explicitly tell it that it’s not allowed to make code changes, but should essentially operate in planning mode.

The agent structure was instantly useful. Agents have their own context, so this allowed the main Claude process to keep its context free of the content of all the information handled by the agent, and it could focus entirely on the source code. I’d often have to remind it to consult with the agent, but putting in explicit guide rails (“for each task in $file, consult the expert to get a detailed plan, then implement that plan, then re-consult with the expert to perform code review, then repeat until approved”) generally got things running really quite nicely, and kept everything on track.

Doing My Part

For my part, I’m basically doing code review. I’ll sometimes let Claude just run & make multiple changes, usually when it’s doing test development, and usually with guard rails: “don’t change library code without my approval, only change the tests. Don’t assume the test expectations are correct, they may not be—ask the expert what the right output should be first.” On thornier problems, I’m often working through logic issues with Claude’s aid, checking its reasoning and expectations, and watching out for hallucination.

Sometimes I get the feeling that Claude is operating in a sort of fuzzy, semi-drunk cloud: it can focus in on the thing in front of it, but can end up guessing about adjacent concepts rather than just looking at the thing to understand it. It’s making a concerted effort, but it gets muddled. It’s my job to remind it to look around and learn, and to make it take notes when necessary.

Sometimes I would need to teach Claude about certain things, like the Swift Synchronization library and its Mutex type, or provide ways of managing state that it hadn’t considered due to its iterative-improvement flow. Things like titles for @Test macros, and tests with multiple inputs rather than multiple near-identical tests were other things it didn’t know about ahead of time.

My other main input has been to manually verify everything it claims is “complete.” As mentioned above, Claude likes to stub things in the middle of larger tasks, and totally forgets about that, or just considers that Good Enough. So I’ve wound up checking how things actually work and getting it to go back and get on track. It churned out a ZIL compiler pretty quickly, aided by some of the Infocom game repositories having both a .zil and a .zap version of the same file. This was actually incredibly helpful, because the expert agent was able to check the rules from documentation against these files to confirm what it learned. The compiler Claude created, though, had a lot of “for now” implementations though, so it was actually missing a whole bunch of functionality. I had to teach it how to determine what was missing and set up a plan.

Results!

The project is now up to version 0.4.3 and available on GitHub for perusal. Right now, the compiler needs a bunch of work (macros are now implemented but not manually tested, and we need better support for creating table-generation assembly), but Claude claims the assembler is fully spec-compliant.

The VM (zil run <game-file>) is actually officially done, at least for ZMachine v3 games like the Zork trilogy, Hitch-Hiker’s Guide to the Galaxy, Planetfall, and Stationfall. I’ve tried a .v5 game and it went tilt, so there’s more to fix in there, but it’s actually usable to play through the whole of Zork I already!

That was actually the result of a new zil autoplay command, which takes a game and an instruction file containing a series of commands to run, along with some directives to deal with special output cases (fights, healing, waiting until some output appears, etc.). By default it’ll wait a bit (depending how much text is in the latest output), then run a command.

There’s a ‘manual’ mode where you can type your own commands and press enter without typing to make it run the next step from the instruction file. You can also provide a number of seconds to wait in-between commands. Setting this to zero with the right set of instructions can get you here in no time at all:

Inside the Barrow
As you enter the barrow, the door closes inexorably behind you. Around you it
is dark, but ahead is an enormous cavern, brightly lit. Through its center
runs a wide stream. Spanning the stream is a small wooden footbridge, and
beyond a path leads into a dark tunnel. Above the bridge, floating in the air,
is a large sign. It reads:  All ye who stand before this bridge have completed
a great and perilous adventure which has tested your wit and courage. You have
mastered the first part of the ZORK trilogy. Those who pass over this bridge
must be prepared to undertake an even greater adventure that will severely
test your skill and bravery!

The ZORK trilogy continues with "ZORK II: The Wizard of Frobozz" and is
completed in "ZORK III: The Dungeon Master."
Your score is 350 (total of 350 points), in 340 moves.
This gives you the rank of Master Adventurer.

Would you like to restart the game from the beginning, restore a saved game
position, or end this session of the game?
(Type RESTART, RESTORE, or QUIT):
>

…and in fact, that’s exactly where this text came from: swift run zil autoplay zork1.z3 zork1-steps --interval 0. Sometimes the Thief in the game messes things up by stealing a treasure and dumping it somewhere random, but at a zero-second command input gap, it takes barely a couple of seconds to try again.

I sincerely hope to get the compiler and assembler working soon, to the point where I can build and run the Zork games directly from source, then get the VM up to scratch for the latest game files.

Lots of fun!

Now I’m going to go play Zork II. Wish me luck…


© 2009-2019. All rights reserved.

Powered by Hydejack v9.2.1