“Black Mirror: Bandersnatch” is an interactive movie where the choices you make determine the protagonist’s fate. The story, which takes place in the 1980’s, is centered on a video game programmer named Stefan who is working on a choose-your-own-adventure game called Bandersnatch, based on the eponymous book by Jerome F. Davies. Stefan meets with the staff of the popular game development company Tuckersoft – including “industry superstar” Colin Ritman. Colin is working on a game called “Nohzdyve” (a play on a previous episode of Black Mirror).
It turns out that Nohzdyve is actually available as a playable game on a website that is revealed as audio data on a hidden ending to the movie. If you record the audio and open it in a ZX Spectrum emulator (a popular game console in the UK in the 1980s), a QR code is revealed that links to this site. On here you will see a button that says “Nohzdyve” (and if you don’t, hit the “History” button until it appears). Clicking this link leads to a page for the game along with a download for a ROM image to play in a ZX Spectrum emulator.
The game is simple: you jump out of a window and fall while avoiding hazards on the way down, including flying teeth and the buildings at the boundaries of the screen. If you fall into an eyeball, you gain ten points. Try to gain as many points as you can with your three lives!
Easter eggs in the game?
Because of the interactive nature of Bandersnatch, the scenes that are revealed depend on the choices the user makes. Thus, nobody is really sure whether or not all the content in the episode has been seen. If you check out the Reddit page for Bandersnatch, you’ll see theories about how to unlock potential easter eggs. I was particularly interested in the Nohzdyve game because it seemed like there were clues on the download page:
- “Perfection is key.”
- “You may become addicted to increasing your score.”
What if I were to hit every eyeball and accumulate a ridiculously large number of points? Would some hidden message be revealed? Would my falling player finally hit the ground and die? I knew the best way to test this theory would be to hack the ROM to get rid of all the hazards and have my player fall continuously into eyeballs while I did something else for a while.
So…I did some research on hacking ZX Spectrum ROMs. Here’s what I did!
Building a Profile
I referred to this guide, which lays out how to use a tool called Skoolkit to disassemble the game and make a map of what memory addresses and variables correspond to certain triggers and actions. The first thing I did was save a snapshot of the game. If you load the
.tap file into your emulator you can save a snapshot as a
.z80 file. This is simply for convenience, as
.z80 files load much faster and we will do a lot of closing and re-opening of the ROM as we discover new features by trial and error.
Next, I built a profile of the game. I used the emulator Fuse, which has a built-in profiler that you can activate using the command
Machine > Profile > Start. Your goal here is to trigger every action possible in the game. Hit one of your enemies on the way down. Hit a wall. Accumulate points. Arrive at a game over screen. This helps builds a map of all the triggers and their locations in memory. When you are done, hit
Machine > Profiler > Stop and save the file (which has a .profile extension).
This is where Skoolkit comes in. It’s a Python library that can perform a number of tasks on Spectrum ROMs, but the programs we’re most interested in are
sna2skool.py. The first program
sna2ctl.py converts the game snapshot to a control file, which contains all the memory addresses and corresponding actions. We can then convert the
.ctl file to the much easier to interpret (but equivalent)
.skool file using the second program
sna2skool.py. Together, the commands are (assuming your profile was saved as
python sna2ctl.py -m nohzdyve.profile nohzdyve.z80 > nohzdyve.ctl python sna2skool.py -c nohzdyve.ctl nohzdyve.z80 > nohzdyve.skool
nohzdyve.skool contains the memory addresses and corresponding commands in ZX Spectrum assembly language. Though assembly language is extremely low level and difficult to program, it is not nearly as bad to interpret in the context of hacking games – when you know what you are looking for. The goal is basically to use the built-in debugger (
Machine -> Debugger) to enable and disable certain commands and observe the effects. There is a lot of trial and error involved, though a few helpful and not-so-arbitrary first steps can be taken to identify key routines that trigger events such as gains in points and losses of life.
POKE Me: An example
A useful tool contained in Fuse is the POKE Finder, accessible through
Machine -> POKE Finder. It allows you to keep track of values of interest and when they change, helping us find the locations of these values in memory. As an example, we will discuss how to use the POKE Finder to obtain infinite lives. Whenever you die, the game pauses until you hit the space bar. Thus, I started the game, intentionally lost my first of three lives, and used the POKE Finder to find memory addresses containing the value “2” for my remaining two lives.
There are 49152 total locations initially, but these were reduced about 100-fold upon entering this constraint. Then I played, died again, and further restricted these locations to those currently holding the value “1” for my one remaining life. After doing this, there is only one memory location left – which must be responsible for storing and updating the number of lives! It occurs on page 2 in RAM, offset $00C1, and one can set a breakpoint there on the debugger via the command
bre wr RAM:$0002:$00C1. When setting a breakpoint at an address in memory, the game will automatically pause when that address is written to and the debugger will display the next commands in line. One can then refer to the
nohzdyve.skool file for an understanding of what commands were triggered before this point in the code was reached.
I will highlight three important lines in the
.skool file (along with the comments I made at the time), which were identified after using the POKE Finder as described above:
$806C LD A,($80C1) ; Load number of lives into register A $806F DEC A ; Decrease A by 1 $8070 JP Z,$8096 ; Num lives zero? Jump to G A M E O V E R
The first line loads the number of lives in
$80C1 into the
A register. This value is decreased by 1 on the following line. Then it jumps to another location in memory if the value contained in
A is zero. If we want infinite lives, then we never want to make the jump to
$8096. If we instead increase
A back to its original value, the number of lives will remain constant. To do so, we can run the debugger command
set $8070 60, where 60 corresponds to the statement
INC A (see this link for a list of operator codes for the ZX Spectrum).
Below is the hack in action. Note that the number of hearts at the top left portion of the screen does not change.
The only problem with making this change via the debugger is that it is irreversible. The memory address
$8070 has lost its original command, and we couldn’t trigger a loss of life if we wanted to. The solution to this is to instead create a POKE file, a list of commands that can be loaded by the emulator and switched on and off as one pleases. I created a file called
nohzdyve.pok and inserted the following:
NInfinite Lives Z 8 32880 60 0 Y
This follows the standard format of a POKE file:
- Each command begins with the letter N followed by the name of the POKE on the first line.
- On the second line is the letter Z, followed by memory bank address (8 just means it can be any).
- Next is the hex address
$8070converted to decimal format.
- The value 60 is the operation code for
INC A, as described above.
- 0 is the value that will be assigned when the POKE is disabled. I find this is usually reversible when 0 is used.
- The last line in the file must be the letter Y.
One can load this file into Fuse via the command
File -> Open File. This pops the POKE into the
POKEs/Cheats dialog (accessible via
Machine -> POKEs/Cheats), with a checkmark to enable or disable it.
Getting to the point…
Now that we’ve covered how to use the debugger and the POKE finder to accomplish our goals, we’ll speed this discussion along. There are three main things we need to do:
- Get rid of the flying teeth
- Prevent the eyes from moving back and forth
- Figure out how to position ourselves directly over the stationary eyes and then fall vertically downward forever.
Once step three is completed, we can let the emulator run while we rack up a LOT of points. We can even run the emulator at 1000% speed. If perfection is really key, as indicated on the Nohzdyve page, then this better do the trick!
Much of this took trial and error. The first task was accomplished by examining the routines connected with changes in the life counter, as hitting the teeth results in death. The second task required using the POKE Finder again to figure out where the total score is stored, as hitting an eye results in an increase in the score. The following POKE file satisfies the first two tasks:
NNo Enemies Z 8 33694 201 0 NNon-moving eyes Z 8 33850 0 0 Y
The operation code
201 is the “return”, or
RET command, which breaks out of a subroutine early. This way we manage to skip some code that generates the flying teeth. The operation code
0 replaces the command with no operation, or
NOP. I believe a subroutine is called at this address that would have updated the position of the eyes, so we cancel it out to keep them stationary. I commonly use these two operation codes (
NOP) to disable subroutines and remove commands to observe the effect on gameplay. Sometimes the game will crash. Other times nothing is directly observable.
For the last task we can use the debugger to manually set the direction of horizontal movement to 0 (i.e. vertical free-fall) via the command
set $8527 $0. This took a lot of trial and error, but the key was to look for sections of the code that required user input. There were only four such sections, and I quickly identified where the direction of movement is updated as the player presses the left and right buttons.
I’ve included a series of GIFs that shows how I achieved “perfection”:
- Load up the POKE file and activate the no enemies + non-moving eyes pokes
- Move directly above the first eye you see and press the “break” button on the debugger.
- Enter the command
set $8527 $0and evaluate it.
- Continue the game and enjoy falling without any hazards.
- Optional step: speed up your emulator to accumulate points super fast!
No Easter Eggs! But…
I’ve accumulated over 100,000 points this way, and there is nothing to be found here! In fact, the designer of the game posted this tweet, which I saw much too late.
To anyone chasing easter eggs in the game that might unlock extra scenes or paths to an ARG… if anything like that exists, I’m not in on the secret. Sorry to everyone who’s been trying to score exactly 20541 points or find messages in the VHS static.
Oh well! This was definitely a fun project, but looking at assembly code for hours can do funny things to the brain. It got me looking for meaning in strange places, like the hex and decimal addresses where important subroutines were called. I was, as Colin Ritman says, “in the hole”. I probably won’t do any reverse engineering of this sort again anytime soon, but I’m happy to hear any suggestions for another project that will stimulate my brain.