The snake game
The same program in M68K, MIPS, RISC-V.
The whole ladder in one program. A snake of green squares crosses a board of 15 by 11 cells, the arrow keys steer it, it grows by one segment every time it reaches the food, and it ends when its head leaves the board or runs into its own body. The score sits above the board while you play and the last one goes into the transcript when you lose.
Click the Screen panel before you press a key, the same as in Move a square with the keyboard, and press Run again to play another game.
The program is 360 lines, so the sections below take it a piece at a time and the whole thing is at the bottom, ready to build and play.
A cell is one byte
The board is 15 columns by 11 rows, and a position on it is one byte: the column in the high
four bits and the row in the low four, so 0x35 is column 3, row 5. Four bits hold a number up to
15, which is what caps the board, and the Screen settles the rest: fifteen columns of 16 pixels is
the 240 the Screen is resized to, and eleven rows of 16 fill what is left under the two text rows.
Packing it this way is what makes the game cheap to write. Comparing two positions is a single cp.
A position that took a whole pair would need an or a and an sbc hl, de every time the snake
checked whether it had hit itself, and that sbc destroys one of the two values it is comparing.
The packing pays again in the drawing. A column sitting in the high four bits is the column
times 16 already, so and 0xF0 is the whole of the x arithmetic, and the row needs four rlca and
the offset of the two text rows above the board. No multiplication happens anywhere in draw_cell,
which matters on a machine that has none.
P_CHAR equ 0x10 ; a character, on the Screen and in the transcript
P_NUM equ 0x11 ; a byte as an unsigned decimal number
P_PEN equ 0x20
P_FILL equ 0x21
P_X equ 0x23
P_Y equ 0x24
P_X2 equ 0x25
P_Y2 equ 0x26
P_CMD equ 0x27
P_COL equ 0x29 ; the text cursor, in 8 by 8 cells
P_ROW equ 0x2A
P_KEY equ 0x31 ; 1 while the key whose code is in b is held down
P_WAIT equ 0x50 ; reading it waits for b hundredths of a second
C_RECT equ 4
C_CLEAR equ 9
C_RESIZE equ 10
C_BUF_ON equ 11
C_PRESENT equ 13
K_LEFT equ 0x25
K_UP equ 0x26
K_RIGHT equ 0x27
K_DOWN equ 0x28
COLS equ 15 ; the board, in cells
ROWS equ 11
CELL equ 16 ; and one cell, in pixels
TOP equ 16 ; the board starts under the two text rows
WIDTH equ COLS * CELL
HEIGHT equ TOP + ROWS * CELL
MAXLEN equ 48
PACE equ 12 ; hundredths of a second per cell
BLACK equ 0x00
SNAKE equ 0x1C
RED equ 0xE0
WHITE equ 0xFF
Setting up, and one frame
The Screen is resized and cleared to black first, because the colour a clear uses also becomes the background the text rows are painted on. Then double buffering goes on, and from there every frame is painted off screen and shown in one go.
A frame is always the same six things: poll the arrows, move the snake, check whether it hit itself,
check whether it reached the food, draw, and wait. Port 0x50 sets the pace: put a number of
hundredths in b, read the port, and the program waits that long without freezing the editor, so
Stop still answers while the snake is between cells.
.org 0x8000
ld a, WIDTH
out (P_X), a
ld a, HEIGHT
out (P_Y), a
ld a, C_RESIZE
out (P_CMD), a
ld a, BLACK
out (P_FILL), a
ld a, C_CLEAR
out (P_CMD), a ; black, and black becomes the text background
ld a, WHITE
out (P_PEN), a
call draw_score
ld a, C_BUF_ON
out (P_CMD), a ; the off-screen image starts as a copy of this one
frame:
The arrows, and the turn you are not allowed to make
The arrows do not move the snake. They call try_direction, which writes dx and dy, and the
move happens later in the frame from whatever those two say.
try_direction refuses one thing: a direction that is the exact opposite of the way the snake is
already going. dx + nx and dy + ny are both zero only when the new direction is backwards, and
or h asks about the two of them in one test. Without that refusal, turning back would mean eating
your own neck on the very next frame.
; --- the arrows, one poll per key -------------------------------------------
ld c, P_KEY
ld b, K_LEFT
in a, (c)
or a
jr z, not_left
ld d, -1
ld e, 0
call try_direction
not_left:
ld b, K_UP
in a, (c)
or a
jr z, not_up
ld d, 0
ld e, -1
call try_direction
not_up:
ld b, K_RIGHT
in a, (c)
or a
jr z, not_right
ld d, 1
ld e, 0
call try_direction
not_right:
ld b, K_DOWN
in a, (c)
or a
jr z, not_down
ld d, 0
ld e, 1
call try_direction
Moving, which is a shift
The snake moves by shifting. Every segment takes the place of the one in front of it, walking from the tail backwards so that nothing is overwritten before it has been read, and then the head is given its new cell. The tail disappears from where it was without any code saying so.
Growing is one extra byte: the new last segment is put on top of the old one, so for a single frame two segments sit in the same cell, and the next shift pulls them apart.
The head's new cell is the old one plus the direction, and dx and dy are counted in cells, so
they are 1, 0 or -1. cp COLS catches both walls at once: a column of 15 fails it the obvious way,
and a column of -1 is FF as an unsigned byte, which is far above 15 and fails it as well. Both
tests have to run before the column is packed back into the high four bits, because once it is
packed, rlca cannot tell -1 from 15.
not_down:
; --- every segment takes the place of the one in front of it -----------------
ld a, (length)
ld e, a
ld d, 0
ld hl, body
add hl, de
dec hl ; hl = the last segment of the body
ld b, e
dec b ; length - 1 copies
jr z, moved ; a snake of one segment has none
shift:
dec hl
ld a, (hl)
inc hl
ld (hl), a ; body[i] = body[i - 1]
dec hl
djnz shift
moved:
; --- the new head, one cell on from the old one ------------------------------
ld a, (body) ; the head: the column in the high nibble
and 0x0F
ld e, a ; row
ld a, (body)
and 0xF0
rrca
rrca
rrca
rrca
ld d, a ; column
ld hl, dx
ld a, d
add a, (hl)
cp COLS
jp nc, game_over ; off the left or the right, since -1 is 255 here
ld d, a
ld hl, dy
ld a, e
add a, (hl)
cp ROWS
jp nc, game_over ; off the top or the bottom
ld e, a
ld a, d
rlca
rlca
rlca
rlca
or e ; the new head, packed back into one byte
ld (body), a
ld c, a
Hitting yourself, and finding the food
The self collision is the single cp the packing bought: walk the body from the second segment on
and compare each one against the head.
The food goes wherever a sixteen bit xorshift says. Three shifts and three xor instructions
turn one number into the next of a sequence that visits all 65535 non-zero values before it repeats,
which is as close to random as a program with no dice can get. Two of the three shifts cost almost
nothing here: x ^ (x << 8) only changes the high byte, because the low half of x << 8 is all
zero, and x ^ (x >> 9) only changes the low byte for the same reason. Only the x << 7 needs a
loop.
The column is masked down to four bits and the one value over the edge of the board is folded back onto the first column, and the row is folded the same way, which is why the first five rows come up about twice as often as the other six.
; --- did it run into itself --------------------------------------------------
ld a, (length)
ld b, a
dec b
jr z, no_bite
ld hl, body
inc hl
ld a, c
bite:
cp (hl)
jp z, game_over
inc hl
djnz bite
no_bite:
; --- did it reach the food ---------------------------------------------------
ld a, (food)
cp c
jr nz, no_meal
ld hl, score
inc (hl)
ld a, (length)
cp MAXLEN
jr nc, no_room
ld e, a
ld d, 0
ld hl, body
add hl, de
dec hl
ld a, (hl)
inc hl
ld (hl), a ; the new tail starts on top of the old one
ld hl, length
inc (hl)
no_room:
call place_food
call draw_score
Drawing the frame
The board is wiped with a rectangle that starts at TOP rather than with command 9, which would
clear the whole image including the score. Then one red square for the food, one green square per
segment, and command 13 to show the lot.
The score goes out through the console character and number ports, the same ones Print a string
used, so it appears on the Screen and in the transcript at once. The Screen has no text command of
its own, so those two rows above the board are the only place text can go. draw_score runs at the
start and once per point, which leaves the transcript with one line per score instead of one per
frame.
no_meal:
; --- draw the whole frame off screen and show it in one go -------------------
ld a, BLACK
out (P_FILL), a
out (P_PEN), a
xor a
out (P_X), a
ld a, TOP
out (P_Y), a
ld a, WIDTH
out (P_X2), a
ld a, HEIGHT
out (P_Y2), a
ld a, C_RECT
out (P_CMD), a ; wipe the board, leaving the score above it
ld a, RED
out (P_FILL), a
out (P_PEN), a
ld a, (food)
call draw_cell
ld a, SNAKE
out (P_FILL), a
ld a, WHITE
out (P_PEN), a
ld hl, body
ld a, (length)
ld b, a
draw_body:
ld a, (hl)
inc hl
call draw_cell
djnz draw_body
ld a, C_PRESENT
out (P_CMD), a ; the frame becomes visible here, all at once
ld b, PACE
ld c, P_WAIT
in a, (c) ; twelve hundredths of a second of program time
jp frame
game_over:
xor a
out (P_COL), a
ld a, 1
out (P_ROW), a
ld hl, over
call print
ld a, (score)
out (P_NUM), a
ld a, 10
out (P_CHAR), a
ld a, C_PRESENT
out (P_CMD), a ; over the last frame, which is still there
halt
; try_direction(nx, ny): nx in d and ny in e. Takes the new direction unless it
; is the exact opposite of the one the snake is going, which would be a bite.
try_direction:
ld a, (dx)
add a, d
ld h, a
ld a, (dy)
add a, e
or h ; both zero only when the new way is backwards
ret z
ld a, d
ld (dx), a
ld a, e
ld (dy), a
ret
; draw_cell(c): the packed cell in a, drawn in the colours already set
draw_cell:
ld c, a
and 0xF0 ; the column is the high nibble, so this is column * 16
out (P_X), a
add a, CELL - 1
out (P_X2), a ; one pixel short, so the cells have a gap
ld a, c
and 0x0F
rlca
rlca
rlca
rlca ; row * 16
add a, TOP
out (P_Y), a
add a, CELL - 1
out (P_Y2), a
ld a, C_RECT
out (P_CMD), a
ret
; draw_score(): the label and the number, on the row the board never covers
draw_score:
xor a
out (P_COL), a
out (P_ROW), a
ld hl, label
call print
ld a, (score)
out (P_NUM), a
ld a, 10
out (P_CHAR), a
ret
; place_food(): a cell nobody chose, out of a sixteen bit generator
place_food:
call next_random
ld a, l
and 0x0F
cp COLS
jr c, got_column
sub COLS ; the sixteenth column folds back onto the first
got_column:
rlca
rlca
rlca
rlca
ld c, a ; the column, in the high nibble already
call next_random
ld a, h
and 0x0F
cp ROWS
jr c, got_row
sub ROWS
got_row:
or c
ld (food), a
ret
; next_random(): the next value of a sixteen bit xorshift, in hl
next_random:
ld hl, (seed)
ld d, h
ld e, l ; de = x
ld b, 7
shift7:
add hl, hl
djnz shift7 ; hl = x << 7
ld a, h
xor d
ld h, a
ld a, l
xor e
ld l, a ; x = x ^ (x << 7)
ld a, h
srl a
xor l
ld l, a ; x = x ^ (x >> 9), whose high half is all zero
ld a, l
xor h
ld h, a ; x = x ^ (x << 8), whose low half is all zero
ld (seed), hl
ret
; print(p): the zero terminated string at hl
print:
ld a, (hl)
or a
ret z
out (P_CHAR), a
inc hl
jr print
The state, in memory
Thirty-odd bytes hold the whole game. body is an array of cells with length saying how much of
it is in use, and .ds MAXLEN-3 reserves the room the snake will grow into.
.org 0x9000
dx: .db 1 ; the direction, in cells
dy: .db 0
length: .db 3
score: .db 0
seed: .dw 0xACE1
food: .db 0x76 ; column 7, row 6
body: .db 0x35, 0x25, 0x15
.ds MAXLEN-3
label: .asciz "SCORE: "
over: .asciz "GAME OVER, SCORE: "
The whole program
Build this one and play it.
P_CHAR equ 0x10 ; a character, on the Screen and in the transcript
P_NUM equ 0x11 ; a byte as an unsigned decimal number
P_PEN equ 0x20
P_FILL equ 0x21
P_X equ 0x23
P_Y equ 0x24
P_X2 equ 0x25
P_Y2 equ 0x26
P_CMD equ 0x27
P_COL equ 0x29 ; the text cursor, in 8 by 8 cells
P_ROW equ 0x2A
P_KEY equ 0x31 ; 1 while the key whose code is in b is held down
P_WAIT equ 0x50 ; reading it waits for b hundredths of a second
C_RECT equ 4
C_CLEAR equ 9
C_RESIZE equ 10
C_BUF_ON equ 11
C_PRESENT equ 13
K_LEFT equ 0x25
K_UP equ 0x26
K_RIGHT equ 0x27
K_DOWN equ 0x28
COLS equ 15 ; the board, in cells
ROWS equ 11
CELL equ 16 ; and one cell, in pixels
TOP equ 16 ; the board starts under the two text rows
WIDTH equ COLS * CELL
HEIGHT equ TOP + ROWS * CELL
MAXLEN equ 48
PACE equ 12 ; hundredths of a second per cell
BLACK equ 0x00
SNAKE equ 0x1C
RED equ 0xE0
WHITE equ 0xFF
.org 0x8000
ld a, WIDTH
out (P_X), a
ld a, HEIGHT
out (P_Y), a
ld a, C_RESIZE
out (P_CMD), a
ld a, BLACK
out (P_FILL), a
ld a, C_CLEAR
out (P_CMD), a ; black, and black becomes the text background
ld a, WHITE
out (P_PEN), a
call draw_score
ld a, C_BUF_ON
out (P_CMD), a ; the off-screen image starts as a copy of this one
frame:
; --- the arrows, one poll per key -------------------------------------------
ld c, P_KEY
ld b, K_LEFT
in a, (c)
or a
jr z, not_left
ld d, -1
ld e, 0
call try_direction
not_left:
ld b, K_UP
in a, (c)
or a
jr z, not_up
ld d, 0
ld e, -1
call try_direction
not_up:
ld b, K_RIGHT
in a, (c)
or a
jr z, not_right
ld d, 1
ld e, 0
call try_direction
not_right:
ld b, K_DOWN
in a, (c)
or a
jr z, not_down
ld d, 0
ld e, 1
call try_direction
not_down:
; --- every segment takes the place of the one in front of it -----------------
ld a, (length)
ld e, a
ld d, 0
ld hl, body
add hl, de
dec hl ; hl = the last segment of the body
ld b, e
dec b ; length - 1 copies
jr z, moved ; a snake of one segment has none
shift:
dec hl
ld a, (hl)
inc hl
ld (hl), a ; body[i] = body[i - 1]
dec hl
djnz shift
moved:
; --- the new head, one cell on from the old one ------------------------------
ld a, (body) ; the head: the column in the high nibble
and 0x0F
ld e, a ; row
ld a, (body)
and 0xF0
rrca
rrca
rrca
rrca
ld d, a ; column
ld hl, dx
ld a, d
add a, (hl)
cp COLS
jp nc, game_over ; off the left or the right, since -1 is 255 here
ld d, a
ld hl, dy
ld a, e
add a, (hl)
cp ROWS
jp nc, game_over ; off the top or the bottom
ld e, a
ld a, d
rlca
rlca
rlca
rlca
or e ; the new head, packed back into one byte
ld (body), a
ld c, a
; --- did it run into itself --------------------------------------------------
ld a, (length)
ld b, a
dec b
jr z, no_bite
ld hl, body
inc hl
ld a, c
bite:
cp (hl)
jp z, game_over
inc hl
djnz bite
no_bite:
; --- did it reach the food ---------------------------------------------------
ld a, (food)
cp c
jr nz, no_meal
ld hl, score
inc (hl)
ld a, (length)
cp MAXLEN
jr nc, no_room
ld e, a
ld d, 0
ld hl, body
add hl, de
dec hl
ld a, (hl)
inc hl
ld (hl), a ; the new tail starts on top of the old one
ld hl, length
inc (hl)
no_room:
call place_food
call draw_score
no_meal:
; --- draw the whole frame off screen and show it in one go -------------------
ld a, BLACK
out (P_FILL), a
out (P_PEN), a
xor a
out (P_X), a
ld a, TOP
out (P_Y), a
ld a, WIDTH
out (P_X2), a
ld a, HEIGHT
out (P_Y2), a
ld a, C_RECT
out (P_CMD), a ; wipe the board, leaving the score above it
ld a, RED
out (P_FILL), a
out (P_PEN), a
ld a, (food)
call draw_cell
ld a, SNAKE
out (P_FILL), a
ld a, WHITE
out (P_PEN), a
ld hl, body
ld a, (length)
ld b, a
draw_body:
ld a, (hl)
inc hl
call draw_cell
djnz draw_body
ld a, C_PRESENT
out (P_CMD), a ; the frame becomes visible here, all at once
ld b, PACE
ld c, P_WAIT
in a, (c) ; twelve hundredths of a second of program time
jp frame
game_over:
xor a
out (P_COL), a
ld a, 1
out (P_ROW), a
ld hl, over
call print
ld a, (score)
out (P_NUM), a
ld a, 10
out (P_CHAR), a
ld a, C_PRESENT
out (P_CMD), a ; over the last frame, which is still there
halt
; try_direction(nx, ny): nx in d and ny in e. Takes the new direction unless it
; is the exact opposite of the one the snake is going, which would be a bite.
try_direction:
ld a, (dx)
add a, d
ld h, a
ld a, (dy)
add a, e
or h ; both zero only when the new way is backwards
ret z
ld a, d
ld (dx), a
ld a, e
ld (dy), a
ret
; draw_cell(c): the packed cell in a, drawn in the colours already set
draw_cell:
ld c, a
and 0xF0 ; the column is the high nibble, so this is column * 16
out (P_X), a
add a, CELL - 1
out (P_X2), a ; one pixel short, so the cells have a gap
ld a, c
and 0x0F
rlca
rlca
rlca
rlca ; row * 16
add a, TOP
out (P_Y), a
add a, CELL - 1
out (P_Y2), a
ld a, C_RECT
out (P_CMD), a
ret
; draw_score(): the label and the number, on the row the board never covers
draw_score:
xor a
out (P_COL), a
out (P_ROW), a
ld hl, label
call print
ld a, (score)
out (P_NUM), a
ld a, 10
out (P_CHAR), a
ret
; place_food(): a cell nobody chose, out of a sixteen bit generator
place_food:
call next_random
ld a, l
and 0x0F
cp COLS
jr c, got_column
sub COLS ; the sixteenth column folds back onto the first
got_column:
rlca
rlca
rlca
rlca
ld c, a ; the column, in the high nibble already
call next_random
ld a, h
and 0x0F
cp ROWS
jr c, got_row
sub ROWS
got_row:
or c
ld (food), a
ret
; next_random(): the next value of a sixteen bit xorshift, in hl
next_random:
ld hl, (seed)
ld d, h
ld e, l ; de = x
ld b, 7
shift7:
add hl, hl
djnz shift7 ; hl = x << 7
ld a, h
xor d
ld h, a
ld a, l
xor e
ld l, a ; x = x ^ (x << 7)
ld a, h
srl a
xor l
ld l, a ; x = x ^ (x >> 9), whose high half is all zero
ld a, l
xor h
ld h, a ; x = x ^ (x << 8), whose low half is all zero
ld (seed), hl
ret
; print(p): the zero terminated string at hl
print:
ld a, (hl)
or a
ret z
out (P_CHAR), a
inc hl
jr print
.org 0x9000
dx: .db 1 ; the direction, in cells
dy: .db 0
length: .db 3
score: .db 0
seed: .dw 0xACE1
food: .db 0x76 ; column 7, row 6
body: .db 0x35, 0x25, 0x15
.ds MAXLEN-3
label: .asciz "SCORE: "
over: .asciz "GAME OVER, SCORE: "