Flappy bird

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.