Flappy bird
The same program in M68K, MIPS, Z80.
A playable flappy bird. The bird falls all the time, one tap of the space bar gives it one flap upwards, and the pipes scroll in from the right with their gaps in a different place every game. It ends the moment the bird touches a pipe or the ground, the ground turns dark red to say so, and another tap starts the next one.
Click the Screen panel before you press a key, the same as in Move a square with the keyboard.
Three ideas in it are worth the read even if you never write a game. A game is a state machine, and this one has three states. The bird's height is counted in sixteenths of a row, because something that can only move in whole rows cannot speed up gradually. And the picture is worked out one column at a time from what the program knows, rather than stored anywhere.
A game is always in one of three states, and s4 says which: READY while the bird hangs still
waiting for the first flap, PLAYING, and DEAD while it drops to the ground. The frame loop reads
the input once, branches on the state, and every branch ends at draw, so one frame is one pass and
the three states differ only in what they do to the bird and the pipes in between.
The read at the top takes every character waiting, not one. The receiver's Ready bit stays set
while the queue has anything in it, so read_key loops until it is clear and s10 remembers whether
any of them was a flap. A player who taps three times while one frame is being drawn gets three
characters and one flap, and a player who holds the key down gets the auto repeat the terminal sends,
which is a flap a few times a second.
Every one of those comparisons costs a li in front of it, because the character to compare
against has to be in a register first. li t2, ' ' then beq t1, t2, flapped is the pair, and it
turns up in front of ble, blt and bge all through the program.
s2 is the bird's height and it counts sixteenths of a row. Gravity adds GRAV, which is 3, to
s3 every frame, and a flap sets s3 to -26: in whole rows those would be 0 and -1, and the bird
would drop at one speed or not accelerate at all. srli s9, s2, 4 is what turns the sixteenths back
into the row the bird is drawn at, and the low four bits that get shifted away are the fractional part
the next frame keeps.
Three pipes make an endless course. move_pipes slides each one one column to the left, and a pipe
whose left edge has gone past -PIPEW, which is one pipe width off the left of the grid, jumps
CYCLE to the right and asks random_gap for a new gap. CYCLE is SPACING * 3, so the pipe lands
exactly where a fourth pipe would have been and the spacing never drifts. The third word of a pipe's
record is whether it has been counted, and it goes up the score, and prints a line, when the pipe's
right edge passes BIRDX.
Nothing is stored about the picture. world_column is given a column and works out from the pipe
records what colour every row of it should be: the pipe down from the top, the gap, the pipe down to
the ground, then the grass and the sand. That is what makes a frame cheap, because only two columns
per pipe can have changed, the one the pipe has just come into and the one it has just left, and
move_pipes repaints exactly those two.
The bird's six columns are repainted in full every frame, and the order they are painted in is the
whole reason the bird does not flicker. fill_span clips every span to a window, the two rows in
t3 and t4, so draw_bird can ask for the world above the bird, then the bird, then the world
below it, and every word of the column is written once with the colour it ends the frame in. Erasing
the band first and drawing the bird into it afterwards would write half of those words twice, and the
display is the picture, so a reader whose browser repainted in between would see the gap.
The window is why draw_bird keeps the bird's top row in s9 rather than in a temporary.
world_column uses t0 to t2, t5 and t6, fill_span uses t0 and t1, and the window
itself takes t3 and t4: every temporary is spoken for, so anything that has to survive a call
goes in a saved register. There are twelve of those, which is what leaves s8, s9 and s10 free
to be locals here, and each subroutine that borrows one saves it next to ra on the way in.
The score goes to the console because the display draws pixels and nothing else. That is also why a
lost game is the ground turning DEADGROUND rather than the words GAME OVER.
random_gap is a 32 bit xorshift. Three shifts and three xor instructions turn a number into
the next one of a sequence, its high bits are the ones worth using, and remu by GAPSPAN puts the
middle of a gap somewhere in the band GAPMIN starts. The seed comes from service 30 on the frame
the first flap happens, so the course depends on when you started playing instead of on a number
written into the program.
HALFGAP is the whole of the difficulty. A gap is measured outwards from its middle in both
directions, by the drawing and by the hit test alike, so lowering it from 9 to 6 makes every gap in
the game narrower and nothing else in the program needs to know.