Exapunking

Making a game for the TEC Redshift

Shaw
Hard Mode
10 min readSep 25, 2018

--

EXAPUNKS is the latest programming game from Zachtronics. Read zines, write viruses, hack everything. Life doesn’t get any better. If you haven’t played it yet then stop reading this right now, go buy it, and come back in a couple days when you have the hang of things.

For those of you who have no interest in playing the game (wtf is wrong with you?!) but still want to grok the rest of this article, here is a bit of context. In EXAPUNKS you program EXAs. EXA stands for EXecution Agent — a small program that can move in and out of networks and execute code. Every EXA has registers and code, can hold a single file, and can communicate with other EXAs inside a network over a global register. You program EXAs in an assembly style language with a very limited set of instructions. EXAs can be uploaded into pretty much any network, regardless of what the underlying hardware is making them extremely versatile. In this article, I will describe how to program EXAs to make a video game for the in-game console called the TEC Redshift.

If you read on, there are some mild spoilers.

Right, now let’s get to hacking. So Ghast has given us a handheld gaming console called the Redshift (much like a Gameboy in this universe) and the second issue of his underground computer magazine Trash World News. It’s up to us to figure out how to program a game for it. I wanted to make something simple for my first Redshift game, so I decided to make a Space Invaders / Galaga style arcade shooter. My game uses three EXAs: one controls the player, one controls the enemies or invaders, and one controls the player’s bullets.

EXA 1 — The Player

The player has a sprite, can move in four directions, and can fire bullets. In order to draw the sprite, we first create a file that contains the data describing which pixels should be toggled on and off. We use the DATA instruction followed by a series of space separated integers to create the file. Each integer is a three digit number: the hundreds place represents whether the pixel is on or off, the tens place represents the x position of the pixel, and the ones place represents the y position of the pixel. A sprite is 10x10 pixels.

DATA 000 010 020 130 140
DATA 150 160 070 080 090
DATA 001 011 121 131 141
DATA 151 161 171 081 091
DATA 002 012 122 132 142
DATA 152 162 172 082 092
DATA 003 013 023 033 143
DATA 153 063 073 083 093
DATA 004 014 024 034 144
DATA 154 064 074 084 094
DATA 005 015 025 135 145
DATA 155 165 075 085 095
DATA 006 116 126 136 146
DATA 156 166 176 186 096
DATA 107 117 027 037 147
DATA 157 067 077 187 197
DATA 108 018 028 138 048
DATA 058 168 078 088 198
DATA 109 019 029 139 049
DATA 059 169 079 089 199

Next, we initialize the collision output register of the EXA so that other EXA’s can identify this one on a collision. We also position the sprite at the bottom center of the Redshift screen and seek to the beginning of the EXA’s held file.

MARK INIT
COPY 1 CO
COPY 55 GX
COPY 90 GY
SEEK -9999

Then, we read each consecutive value from the file into the GP register of the EXA to draw the sprite. After each copy, we test to see if we have reached the end of the file and if not, jump back to the SPRITE label.

MARK SPRITE 
COPY F GP
TEST EOF
FJMP SPRITE
reading the file into the GP register

Main Loop

Once the sprite is drawn, we are ready to start the main loop. We first check for user input by reading from the Redshift’s hardware registers. If input is detected, we jump to the corresponding label to handle either movement or shooting. We also check for collision with an enemy. I will describe how we handle a collision later. For movement, we simply increment or decrement the sprite’s position by writing to the GX or GY register. Vertical movement is pretty useless for this particular game, but I still included it. To shoot a bullet we copy the sprite’s x and y positions to the global M register. Another EXA is listening for values on this register and will handle firing the bullet. I will describe this in detail in one of the following sections. We copy the sprite’s position so that the EXA that handles the bullet knows where to fire the bullet from. After handling all possible user input, collisions, and updating position, we use the WAIT instruction to pause execution until the next frame synchronization (30 times per second) and finally jump to the INPUT label continuing the game loop.

MARK INPUT
LINK 800
NOTE SHOOTING
SWIZ #PADB 1 X
TEST X = 1
TJMP SHOOT
NOTE COLLISION
TEST CI = 2
TJMP GAMEOVER
NOTE HORIZONTAL
TEST #PADX = -1
TJMP MOVL
TEST #PADX = 1
TJMP MOVR
NOTE VERTICAL
TEST #PADY = -1
TJMP MOVU
TEST #PADY = 1
TJMP MOVD
JUMP SYNC
MARK MOVL
SUBI GX 1 GX
JUMP SYNC
MARK MOVR
ADDI GX 1 GX
JUMP SYNC
MARK MOVU
SUBI GY 1 GY
JUMP SYNC
MARK MOVD
ADDI GY 1 GY
JUMP SYNC
MARK SHOOT
COPY GX M
COPY GY M
JUMP SYNC
MARK SYNC
LINK -1
WAIT
JUMP INPUT
player movement

Collision

In the case of a collision with an invader, we jump to the GAMEOVER label. This code moves the player sprite off screen and writes the text ‘GAME OVER’. In order to do this we replicate the EXA 8 times, one for each letter in ‘GAME OVER’. The REPL instruction creates a copy of the EXA and jumps to the specified label in the replicant EXA. Each EXA renders its letter and then loops, doing nothing until the user presses the X button to restart the game. The original EXA jumps to the RESET label, listening for the user to press the X button to restart the game. When the user presses X, the EXA writes a value to the global M register to notify the invader EXA that the game has restarted.

MARK GAMEOVER
COPY 35 GY
REPL G
REPL A
REPL M
REPL E1
REPL O
REPL V
REPL E2
REPL R
COPY -99 GY
JUMP RESET
MARK G
COPY 10 GX
COPY 307 GP
JUMP END
MARK A
COPY 20 GX
COPY 301 GP
JUMP END
MARK M
COPY 30 GX
COPY 313 GP
JUMP END
MARK E1
COPY 40 GX
COPY 305 GP
JUMP END
MARK O
COPY 60 GX
COPY 327 GP
JUMP END
MARK V
COPY 70 GX
COPY 322 GP
JUMP END
MARK E2
COPY 80 GX
COPY 305 GP
JUMP END
MARK R
COPY 90 GX
COPY 318 GP
MARK END
TEST #PADB = 0
TJMP END
HALT
MARK RESET
TEST #PADB = 0
FJMP NEWGAME
JUMP RESET
MARK NEWGAME
COPY 1 M
LINK -1
JUMP INIT

Thats it for the player EXA. Here is the complete code before we move on to the code for the invaders.

DATA 000 010 020 130 140
DATA 150 160 070 080 090
DATA 001 011 121 131 141
DATA 151 161 171 081 091
DATA 002 012 122 132 142
DATA 152 162 172 082 092
DATA 003 013 023 033 143
DATA 153 063 073 083 093
DATA 004 014 024 034 144
DATA 154 064 074 084 094
DATA 005 015 025 135 145
DATA 155 165 075 085 095
DATA 006 116 126 136 146
DATA 156 166 176 186 096
DATA 107 117 027 037 147
DATA 157 067 077 187 197
DATA 108 018 028 138 048
DATA 058 168 078 088 198
DATA 109 019 029 139 049
DATA 059 169 079 089 199
MARK INIT
COPY 1 CO
COPY 55 GX
COPY 90 GY
SEEK -9999
MARK SPRITE
COPY F GP
TEST EOF
FJMP SPRITE
MARK INPUT
LINK 800
NOTE SHOOT
SWIZ #PADB 1 X
TEST X = 1
TJMP SHOOT
NOTE COLLISION
TEST CI = 2
TJMP GAMEOVER
NOTE HORIZONTAL
TEST #PADX = -1
TJMP MOVL
TEST #PADX = 1
TJMP MOVR
NOTE VERTICAL
TEST #PADY = -1
TJMP MOVU
TEST #PADY = 1
TJMP MOVD
JUMP SYNC
MARK MOVL
SUBI GX 1 GX
JUMP SYNC
MARK MOVR
ADDI GX 1 GX
JUMP SYNC
MARK MOVU
SUBI GY 1 GY
JUMP SYNC
MARK MOVD
ADDI GY 1 GY
JUMP SYNC
MARK SHOOT
COPY GX M
COPY GY M
JUMP SYNC
MARK SYNC
LINK -1
WAIT
JUMP INPUT
MARK GAMEOVER
COPY 35 GY
REPL G
REPL A
REPL M
REPL E1
REPL O
REPL V
REPL E2
REPL R
COPY -99 GY
JUMP RESET
MARK G
COPY 10 GX
COPY 307 GP
JUMP END
MARK A
COPY 20 GX
COPY 301 GP
JUMP END
MARK M
COPY 30 GX
COPY 313 GP
JUMP END
MARK E1
COPY 40 GX
COPY 305 GP
JUMP END
MARK O
COPY 60 GX
COPY 327 GP
JUMP END
MARK V
COPY 70 GX
COPY 322 GP
JUMP END
MARK E2
COPY 80 GX
COPY 305 GP
JUMP END
MARK R
COPY 90 GX
COPY 318 GP
MARK END
TEST #PADB = 0
TJMP END
HALT
MARK RESET
TEST #PADB = 0
FJMP NEWGAME
JUMP RESET
MARK NEWGAME
COPY 1 M
LINK -1
JUMP INIT

EXA 2 — The Invaders

The invaders have a UFO-like sprite and endlessly spawn at the top of the screen, diving down towards the player. Just like the player EXA, the enemy EXA first creates a file that defines the sprite, initializes the collision output register, and loops through the file writing values to the GP register to draw the sprite.

DATA 000 010 020 130 140
DATA 150 160 070 080 090
DATA 001 011 121 131 141
DATA 151 161 171 081 091
DATA 002 012 122 132 142
DATA 152 162 172 082 092
DATA 003 013 023 133 143
DATA 153 163 073 083 093
NOTE INIT
COPY 2 CO
LINK 800
MARK SPRITE
COPY F GP
TEST EOF
FJMP SPRITE

Main Loop

Once the sprite is drawn, we begin the invasion. There are actually two loops that control the invader behavior. The outer loop spawns a new invader, and the inner loop moves the invader and tests for collisions. For the outer loop, we use the RAND instruction to generate a random value between 0 and 110 (the Redshift’s screen is 120x100 pixels) and write this value to the GX register to spawn an invader at a random x position at the top of the screen. For the inner loop, we move the invader down the screen by incrementing the GY register. Then, we test for collisions with the player or with a bullet. If there is a player collision, we jump to the KILLPLAYER label to handle this case. If there is a bullet collision, we simply jump to the INVADE label to spawn a new invader. Finally, we test if the invader is offscreen. If it is, we jump to the INVADE label to spawn a new invader, otherwise we jump to the MOVE label to continue the inner loop.

MARK INVADE
RAND 0 110 GX
COPY 0 GY
MARK MOVE
ADDI GY 1 GY
NOTE TEST COLLISION
TEST CI = 1
TJMP KILLPLAYER
TEST CI = 3
TJMP INVADE
NOTE TEST OFFSCREEN
TEST GY > 99
TJMP INVADE
WAIT
JUMP MOVE
invaders spawning

Player Collision

In the case of a player collision, we hide the invader sprite by writing an offscreen value to the GX register, then listen on the global M register for a message from the player EXA to signal that the game has been restarted. We use the statement TEST MRD to test if the EXA can read from the global M register without pausing / blocking. If there is a value in the M register, we discard the value so that the sender can continue executing its code and jump to the INVADE instruction to restart the main loop.

MARK KILLPLAYER
NOOP
NOOP
NOOP
NOOP
NOOP
COPY -77 GX
TEST MRD
TJMP RESET
JUMP KILLPLAYER
MARK RESET
VOID M
JUMP INVADE

*NOTE — the NOOP instructions are there to deal with a race condition. When there is a collision with the player and an invader, the invader receives the message first. The NOOP instructions pause the invader so that the player has time to catch up and receive the collision message before the invader is positioned offscreen. This is maybe a bit hacky but were EXAPUNKING here, hacking by definition.

EXA 3 — The Bullets

The EXA that controls bullets follows the same initialization pattern as the other two EXAs. It draws a 2x2 square sprite and defines the collision output value as 3.

DATA 140 150 141 151COPY 3 CO
MARK SPRITE
COPY F GP
TEST EOF
FJMP SPRITE

Like the invader EXA, the bullet EXA also has two loops: the outer loop waits for a message from the Player to shoot a bullet and the inner loop moves the bullet and tests for a collision with an invader. We also test if the player tries to shoot more than one bullet at a time, in which case we simply ignore the message to fire a bullet by discarding the value from the M register and jumping back to the MOVE label.

MARK WAIT4SHOOT
COPY -99 GX
COPY -99 GY
COPY M GX
COPY M GY
MARK MOVE
NOTE IGNORE MORE THAN 1
TEST MRD
TJMP IGNORE
SUBI GY 1 GY
NOTE TEST COLLISION
TEST CI = 2
TJMP WAIT4SHOOT
NOTE TEST OFFSCREEN
TEST GY < 1
TJMP WAIT4SHOOT
WAIT
JUMP MOVE
MARK IGNORE
VOID M
JUMP MOVE

GG

And that, my friends, is how you properly EXAPUNK.

EXAPUNKING

Thanks for reading! I hope you found it interesting or helpful. If you’re playing EXAPUNKS find me on steam (bitwitch) and we can go head to head in a hacker battle!

--

--

Shaw
Hard Mode

programming sorcery and black magic bit witchery