The snake game
The same program in M68K, RISC-V, Z80.
A green snake moves across a 32 by 32 board. Reach the orange food to score and grow; hitting a wall or your own body ends the game. After 64 segments, food still raises the score, but the snake cannot grow any longer. The board turns dark red on game over.
Select Open in editor, then Build and Run. Click the Screen before typing lowercase
w, a, s or d to steer. Click it again if you return to the editor. Press Run again for a
new game. Each cell is one word in display; the # @screen line connects those words to the Screen.
# @screen unit=16 width=512 height=512 base=display
.eqv MMIO 0xffff0000
.eqv SIDE 32 # the board, in cells
.eqv LAST 31 # SIDE - 1
.eqv CELLS 1024 # SIDE * SIDE
.eqv MAXLEN 64
.eqv FRAME 120 # milliseconds per cell
.eqv BACKGROUND 0x00101820
.eqv SNAKE 0x0040D040
.eqv FOOD 0x00FF4000
.eqv DEAD 0x00400810
.data
display: .space 4096 # CELLS words, four bytes each
body: .word 0x050C, 0x040C, 0x030C
.space 244 # room for MAXLEN segments in all
length: .word 3
dx: .word 1 # the direction, in cells
dy: .word 0
score: .word 0
seed: .word 0x1F123BB5
food: .word 0x140C # column 20, row 12
label: .asciiz "Score: "
over: .asciiz "Game over. Score: "
.text
.globl main
main:
la $s1, display
li $s7, MMIO
li $s5, 0 # nothing has grown yet
li $s0, BACKGROUND # the board, painted once
jal fill_grid
li $s0, SNAKE # and the snake it starts with
la $s2, body
lw $s6, length
start_body:
lw $a0, 0($s2)
jal draw_cell
addi $s2, $s2, 4
addi $s6, $s6, -1
bnez $s6, start_body
li $s0, FOOD
lw $a0, food
jal draw_cell
frame:
# --- one character, and it only ever sets the direction ----------------------
lw $t4, 0($s7) # the receiver control register
andi $t4, $t4, 1 # the Ready bit
beqz $t4, no_key
lw $t5, 4($s7) # the receiver data, which takes the character
andi $t5, $t5, 0xFF
bne $t5, 'a', not_a
li $a0, -1
li $a1, 0
jal try_direction
not_a:
bne $t5, 'd', not_d
li $a0, 1
li $a1, 0
jal try_direction
not_d:
bne $t5, 'w', not_w
li $a0, 0
li $a1, -1
jal try_direction
not_w:
bne $t5, 's', no_key
li $a0, 0
li $a1, 1
jal try_direction
no_key:
# --- every segment takes the place of the one in front of it -----------------
lw $t0, length
sll $t0, $t0, 2
la $t1, body
add $t1, $t1, $t0
addi $t1, $t1, -4 # &body[length - 1], the tail
lw $s3, 0($t1) # the cell the tail is leaving
lw $t2, length
addi $t2, $t2, -1 # length - 1 copies to make
beqz $t2, moved
shift:
lw $t3, -4($t1) # body[i] = body[i - 1]
sw $t3, 0($t1)
addi $t1, $t1, -4
addi $t2, $t2, -1
bnez $t2, shift
moved:
# --- the new head, one cell on from the old one ------------------------------
lw $t4, body # the head, x in the high byte and y in the low
srl $t5, $t4, 8 # x
andi $t6, $t4, 0xFF # y
lw $t7, dx
add $t5, $t5, $t7
lw $t7, dy
add $t6, $t6, $t7
bltz $t5, game_over # off the left
bgt $t5, LAST, game_over
bltz $t6, game_over
bgt $t6, LAST, game_over
sll $t5, $t5, 8
or $s4, $t5, $t6 # the new head, packed again
sw $s4, body
# --- did it run into itself --------------------------------------------------
la $t1, body
addi $t1, $t1, 4
lw $t2, length
addi $t2, $t2, -1
beqz $t2, no_bite
bite:
lw $t3, 0($t1)
beq $t3, $s4, game_over
addi $t1, $t1, 4
addi $t2, $t2, -1
bnez $t2, bite
no_bite:
# --- did it reach the food ---------------------------------------------------
lw $t3, food
bne $t3, $s4, no_meal
lw $t4, score
addi $t4, $t4, 1
sw $t4, score
lw $t5, length
bge $t5, MAXLEN, no_room
sll $t6, $t5, 2
la $t7, body
add $t7, $t7, $t6
sw $s3, 0($t7) # the tail that was leaving stays on instead
addi $t5, $t5, 1
sw $t5, length
li $s5, 1 # so nothing is erased this frame
no_room:
jal place_food
jal print_score
no_meal:
# --- erase the old tail unless it grew; draw the head and food --------------
beqz $s5, erase_tail
li $s5, 0 # it grew, so the tail stays where it is
j tail_done
erase_tail:
li $s0, BACKGROUND
move $a0, $s3
jal draw_cell
tail_done:
li $s0, SNAKE
move $a0, $s4
jal draw_cell
li $s0, FOOD
lw $a0, food
jal draw_cell
li $v0, 32 # a frame of program time
li $a0, FRAME
syscall
j frame
game_over:
li $s0, DEAD
jal fill_grid
li $v0, 4
la $a0, over
syscall
li $v0, 1
lw $a0, score
syscall
li $v0, 11
li $a0, '\n'
syscall
li $v0, 10
syscall
# fill_grid(): every word of the grid becomes the colour in $s0
fill_grid:
move $t0, $s1
li $t1, CELLS
fill_next:
sw $s0, 0($t0)
addi $t0, $t0, 4
addi $t1, $t1, -1
bnez $t1, fill_next
jr $ra
# draw_cell(c): the packed cell in $a0, painted in the colour in $s0.
# It destroys $t0 and $t1 and nothing else.
draw_cell:
srl $t0, $a0, 8 # x
andi $t1, $a0, 0xFF # y
sll $t1, $t1, 5 # y * SIDE
add $t0, $t0, $t1 # + x
sll $t0, $t0, 2 # four bytes per word
add $t0, $t0, $s1
sw $s0, 0($t0)
jr $ra
# try_direction(nx, ny): take the new direction unless it turns the snake back
# on itself, which would be an instant bite. It destroys $t8 and nothing else.
try_direction:
lw $t8, dx
add $t8, $t8, $a0
bnez $t8, take_it
lw $t8, dy
add $t8, $t8, $a1
beqz $t8, no_turn # both zero means the new way is the opposite one
take_it:
sw $a0, dx
sw $a1, dy
no_turn:
jr $ra
# place_food(): retry until the candidate is outside the occupied body.
# It calls next_random, so it saves $ra. next_random changes only $v0 and $t9.
place_food:
addi $sp, $sp, -4
sw $ra, 0($sp)
food_candidate:
jal next_random
andi $t0, $v0, LAST # a column, 0 to 31
sll $t0, $t0, 8
jal next_random
andi $t1, $v0, LAST # a row, 0 to 31
or $t0, $t0, $t1
la $t1, body
lw $t2, length
food_check:
lw $t3, 0($t1)
beq $t0, $t3, food_candidate
addi $t1, $t1, 4
addi $t2, $t2, -1
bnez $t2, food_check
sw $t0, food
lw $ra, 0($sp)
addi $sp, $sp, 4
jr $ra
# next_random(): the next number of a 32 bit xorshift, in $v0
next_random:
lw $v0, seed
bnez $v0, random_step # zero would stay zero through every xor and shift
li $v0, 0x1F123BB5 # recover if the seed was changed to zero
random_step:
sll $t9, $v0, 13
xor $v0, $v0, $t9 # x = x ^ (x << 13)
srl $t9, $v0, 17
xor $v0, $v0, $t9 # x = x ^ (x >> 17)
sll $t9, $v0, 5
xor $v0, $v0, $t9 # x = x ^ (x << 5)
sw $v0, seed
jr $ra
# print_score(): the label and the number, on the console
print_score:
li $v0, 4
la $a0, label
syscall
li $v0, 1
lw $a0, score
syscall
li $v0, 11
li $a0, '\n'
syscall
jr $ra
Follow one frame from frame down to the wait and jump back. It reads at most one key, shifts the
body, computes a new head, checks for a crash or food, and updates the Screen. The routines below
game_over do the drawing, direction check, food placement and score printing.
Cells and movement
body is an array of words, one cell per segment. body[0] is the head, and length says how many
entries are occupied. Bits 8–15 hold column x; bits 0–7 hold row y. For example, 0x050C
decodes to column 0x05 (5), row 0x0C (12). The next two initial entries, 0x040C and
0x030C, put the body immediately to its left. One packed word is enough to compare two cells with
beq.
The shift loop starts at body[length - 1] and copies each preceding entry toward the tail. If
the three entries are [H, A, T], shifting produces [H, H, A]; writing the next head N at
index 0 produces [N, H, A]. Copying backwards preserves each source until it has been read. Before
the shift, the code saves the old tail T in $s3. It has disappeared from the occupied body
array, but its old cell is still coloured on the Screen until erase_tail paints it with the
background colour.
If N reaches food and length is below MAXLEN, the program appends saved T, giving
[N, H, A, T], and increments length. $s5 tells the drawing code to skip erasing that tail
cell. At MAXLEN, a meal still increases score and moves the food, but the body stays the same
length and the old tail is erased.
The old tail is absent from the shifted body when the bite loop checks for a collision. That lets
the head enter the cell the tail just vacated on a normal move. The loop compares the new head with
each remaining body entry; a match ends the game. The head's column and row are calculated
separately from the old head plus dx and dy. A direction such as (1, 0) moves one cell right
per frame. Wall checks happen before packing: a column of -1 would otherwise become a large
positive bit pattern and lose its useful meaning as an out-of-bounds coordinate.
draw_cell turns a packed cell into a word address: display + (y * 32 + x) * 4. For the starting
head at (5, 12), the offset is (12 * 32 + 5) * 4 = 1556 bytes. Each word stores a colour. The
program paints all 1024 background cells once, then draws the starting snake and food. An ordinary
frame stores the background at the old tail, green at the new head, and orange at the food (the
last store usually repaints an unchanged cell). A growing frame skips the tail store, so it makes
two Screen stores. At maximum length, a meal makes all three stores again.
Keys and food
$s7 holds the keyboard receiver address 0xffff0000. Each frame loads receiver control and
checks Ready bit 0. If Ready is 1, the load at 4($s7) reads and consumes one waiting character.
That character can change the stored direction; the snake moves once per frame even when no key is
pressed. The keys set (dx, dy) to left (-1, 0), right (1, 0), up (0, -1) or down (0, 1).
try_direction rejects a reverse turn: while moving right (1, 0), pressing a requests
(-1, 0). The sums 1 + (-1) and 0 + 0 are both zero, so the old direction stays in place.
The first food is hand-written at 0x140C, or (20, 12). After a meal, next_random changes the
stored seed with shifts and xor operations. A zero seed would produce only zeros, so the routine
replaces it with the nonzero starting seed before taking the next step. This is a repeatable
sequence of numbers, not a fresh physical source of randomness. place_food calls it once to
choose a column and again to choose a row. After each call, andi with 31 keeps the coordinate in
the range 0–31. The routine then checks the packed candidate
against every occupied body entry. If it finds a match, it generates another pair. Since the
snake has at most 64 segments on a 1024-cell board, there are always free cells available.
The score prints to the Console after each meal and at game over. The bitmap Screen draws coloured
cells; it has no built-in service to place text, so displaying digits there would require drawing
their shapes from cells. fill_grid and draw_cell use the colour in $s0 and the grid base in
$s1. draw_cell changes only $t0 and $t1, allowing the starting-body loop to keep its pointer
in $s2 across calls. place_food saves $ra on the stack because it calls next_random and must
still return to its own caller afterward.
Try it
Change the initial food: .word 0x140C to food: .word 0x080C. Decode the new word first:
x = 8, y = 12. Build and Run. The orange cell should appear three columns to the right of
the starting head, and, with no keys pressed, the score should print Score: 1 soon after the
snake reaches it. The next food is placed by place_food. Return to 0x140C if you want the
original starting board.
For a second experiment, change only seed: .word 0x1F123BB5, then run the same starting board
twice. The first food remains at its hand-written location; after eating it, both runs with the
same seed place the next food in the same cell. Changing the seed changes the generated sequence.
You can steer the snake while it runs; the fixed testcase has no typed input, so it only exercises
the default rightward path.