Game Boy Port of Snake in Assembly

ibobev1 pts0 comments

Game Boy port of Snake in Assembly

Game Boy port of Snake in Assembly<br>Published: 01 February 2026<br>A port of a classic Snake game for the Nintendo Game Boy, written in GBZ80 ASM. Covers the state machine, snake ring buffer, occupancy grid collision detection, and VBlank-safe rendering.

The classic Snake game is a perfect target for learning Game Boy development. Most people know it from the version preloaded on Nokia phones in the late 1990s, but the concept dates back much further, to arcade games like Blockade (1976)[05]. It requires handling input, managing game state, detecting collisions, and rendering graphics, all within the constraints of an 8-bit CPU running at around 4 MHz with just 8 KB of RAM[02].<br>This post is based on ROM version 1.1 of a complete Snake implementation written in GBZ80 assembly[01]. Rather than walking through every line, it focuses on the most important architectural decisions and hardware-specific techniques. Note that the demo below includes newer iterations of the game and may differ from what is described here.<br>Below is the actual ROM running in a browser-based emulator. You can use the version selector to switch between releases and see how the game evolved across iterations.<br>Overview<br>The game is structured around three main screens managed by a state machine:<br>Title Screen : Displays “SNAKE” and a blinking “PRESS START” prompt.Play Screen : The active game where the snake moves and eats food.Game Over Screen : Shows “GAME OVER” and waits for restart.The core gameplay systems include:<br>Ring Buffer : The snake body is stored as a circular array of coordinates.Occupancy Grid : A 20x18 byte array tracks which cells contain snake, food, or are empty.Dirty Rendering : Only changed tiles are updated during VBlank to avoid screen tearing.Input Handling : D-pad controls with reverse direction prevention.Constants and Configuration<br>The game begins with hardware definitions[04] and configuration constants:<br>INCLUDE "hardware.inc" ; Common hardware symbols (RGBDS)

DEF PLAY_W EQU 20 ; Visible playfield width (tiles)<br>DEF PLAY_H EQU 18 ; Visible playfield height (tiles)

DEF X_MAX EQU (PLAY_W-2) ; Inner max X (18)<br>DEF Y_MAX EQU (PLAY_H-2) ; Inner max Y (16)

DEF MAX_SNAKE EQU 64 ; Must be power-of-two for AND wrap<br>DEF SNAKE_MASK EQU (MAX_SNAKE-1) ; 63<br>DEF SPEED_FRAMES EQU 8 ; Move every N frames

The playfield is 20x18 tiles, matching the Game Boy’s visible screen area. The snake can grow up to 64 segments, chosen as a power of two so we can use a simple AND operation for ring buffer wrapping instead of expensive division or modulo.<br>Game states and directions are defined as simple numeric constants:<br>DEF STATE_TITLE EQU 0 ; Title<br>DEF STATE_PLAY EQU 1 ; Play<br>DEF STATE_OVER EQU 2 ; Game over

DEF DIR_UP EQU 0 ; Up<br>DEF DIR_RIGHT EQU 1 ; Right<br>DEF DIR_DOWN EQU 2 ; Down<br>DEF DIR_LEFT EQU 3 ; Left

The direction values are arranged so that opposite directions differ by 2. This allows a simple XOR trick to detect reverse direction attempts:<br>ld a, [wDir] ; current direction<br>xor 2 ; opposite direction<br>cp b ; compare with candidate<br>ret z ; ignore if reverse

Initialization<br>The entry point sets up the hardware and prepares the initial game state:<br>Start: ; Entry point<br>di ; No interrupts<br>ld sp, $DFF0 ; Stack<br>xor a ; A = 0<br>ld [rNR52], a ; Sound off<br>ld a, [rLCDC] ; LCDC<br>res 7, a ; LCD OFF<br>ld [rLCDC], a ; Write<br>xor a ; A = 0<br>ld [rSCX], a ; SCX=0<br>ld [rSCY], a ; SCY=0<br>ld a, $E4 ; Palette<br>ld [rBGP], a ; Set palette

Interrupts are disabled since the game uses polling rather than interrupt- driven input. The LCD is turned off so we can safely write to VRAM without timing constraints. The scroll registers are zeroed to show the top-left corner of the background map.<br>The palette value $E4 (binary 11100100) sets up four shades:<br>Color 0Lightest (binary 00)Color 1Light (binary 01)Color 2Dark (binary 10)Color 3Darkest (binary 11)Next, tile graphics are copied to VRAM and the random number generator is seeded:<br>ld hl, TileData ; HL = tile ROM<br>ld de, _TileVRAM ; DE = tile VRAM<br>ld bc, TileDataEnd - TileData ; BC = bytes<br>call Memcpy ; Copy tiles (LCD off safe)

ld a, [rDIV] ; Seed<br>or $5A ; Mix<br>ld [wRand], a ; Store RNG

The DIV register is a free-running timer that increments every 256 clock cycles. Reading it at startup provides some randomness based on how long the boot ROM took and when the user started the game.<br>The Main Loop<br>The main loop follows a simple pattern: wait for VBlank, handle rendering, then process game logic:<br>MainLoop: ; Frame loop<br>call WaitVBlankStart ; Enter VBlank<br>call DoPendingRebuildInVBlank ; Handle queued rebuilds (LCD off bulk)<br>call TitleBlinkInVBlank ; Title blink (few writes)<br>call RenderDirtyInVBlank ; Play dirty updates (few writes)<br>call PauseOverlayInVBlank ; Pause overlay (few writes)<br>call WaitVBlankEnd ; Exit VBlank

call ReadJoypad ; Poll joypad

ld a, [wState] ; A = state<br>cp STATE_TITLE ; Title?<br>jr z, .S_Title ; Branch<br>cp STATE_PLAY ; Play?<br>jr z, .S_Play ; Branch<br>jr .S_Over ; Else...

game snake call state vblank hardware

Related Articles