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.