The snake game
The whole ladder in one program: a board of cells packed into words, a body moved by a loop, the keyboard polled every frame, food from a generator of its own, and two words of the screen changing per frame.
The whole ladder in one program. A snake of green cells crosses a board of 32 by 32, the w, a, s
and d 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 goes to the console as you play, and the
board turns dark red 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.
You need to know: everything above it on the ladder. The body is an array walked with a pointer and moved with a loop, the drawing and the generator are subroutines, the keyboard is polled the way Move a square with the keyboard polls it, and only what changed is redrawn the way A bouncing ball does it. What is new is the board kept as cells, one byte for the column and one for the row packed into a word, which becomes an address only at the moment something is drawn.
body is an array of words, one per segment, with the head at body[0], and a segment is a cell:
0x050C is column 5, row 12. Packing the two into one word is what makes a comparison between two
cells a single beq, which the self collision test does once per segment and the food test does once
per frame.
The snake moves by shifting: every segment takes the place of the one in front of it, 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 therefore disappears from where it was without any code saying so, which is why the
cell it was in is read into s3 before the shift runs.
Growing is that same word put back. When the head reaches the food, the cell the tail was leaving is
written one place past the end of the body and length goes up by one, so the segment that was about
to vanish stays where it is. s5 then says the tail did not move this frame, and the drawing skips
the erase.
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. The four wall tests run on t1 and t2 while they are still separate numbers,
because a column of -1 packed back into a byte is 255 and no test after the slli could tell the two
apart.
The keys do not move the snake, they call try_direction, and it refuses a direction that is the
exact opposite of the one the snake is going: dx + nx and dy + ny are both zero only when the new
way is backwards, and turning back means eating your own neck on the next frame.
The food goes wherever a 32 bit xorshift generator says. Three shifts and three xor instructions
turn a number into the next one of a sequence, which is as random as a program with no clock and no
dice can be. Both coordinates are andi with 31, since 32 is a power of two and the low five bits of
any number are already a column.
Three sw instructions reach the screen in a frame and two of them change anything: the cell the tail
left, the cell the head arrived in, and the food, which is repainted whether it moved or not. The M68K
version of this game redraws every segment of the snake into an off screen image and shows the whole
image at once, because that machine has a task for drawing off screen; the bitmap display is the
picture itself, so the cheapest correct frame is the one that writes the fewest words. A frame here is
105 instructions, where clearing the board and redrawing everything would be over four thousand:
fill_grid alone is 1024 passes of four instructions.
Every variable this program keeps between frames lives in memory, not in a register, and a store to
one of them is written with three operands: sw t4, score, t0 puts the score away and uses t0 to
work the address out in, because RISC-V has no $at for the assembler to borrow. MIPS writes
sw $t4, score and spends the hidden register without saying so. The same reason puts the four key
codes in s8 to s11 and MAXLEN in t5 before the branch that reads it: every RISC-V branch
compares two registers and none of them takes a number.
The score is printed to the console, and it is printed there because there is nowhere else to put it.
The M68K draws it onto the screen with a task that puts text at a pixel position; the bitmap display
has no text of any kind. Apart from the wait that paces a frame, every ecall in this program is
printing or the exit at the end.
fill_grid and draw_cell both read the colour out of s1 and the base of the grid out of s0, and
draw_cell promises to destroy t0 and t1 and nothing else, which is what lets the loop that draws
the starting body keep its pointer in t2 and the caller keep the head in s4. Those comments above
the labels are the whole of the agreement, and this program has four of them.
With nobody typing, the snake runs straight to the right, eats the food on the way, and hits the wall
26 frames later, and the red board and the console line are done by 11224 instructions. That is the
game the verification run plays. The runFor of 100000 is the budget the Playground gets before it
stops; a game you are playing ends when you make it end.
Try changing seed: .word 0x1F123BB5 to 0x2545F491. The first food is written into the data
section and does not move, but the one after it falls at column 26 of row 11 instead of column 7 of
row 8. The sequence is fixed by where it starts, so the same program run twice gives the same game
twice, which is what makes a program with a generator like this one debuggable at all.