Flappy bird
The same program in M68K, RISC-V, 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.
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 $t8 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.
$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. srl $t7, $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.
The picture itself is never stored. 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
$t8 and $t9, 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.
fill_span promises to destroy $t0 and $t1 and nothing else, and random_gap promises $v0 and
$t9, which is what lets move_pipes keep a pipe's new left edge in $t7 across a call to the
generator. $s0 is the one saved register the loops have left, so the three subroutines that need a
pointer of their own save it on the stack next to $ra and put it back, which is the calling
convention doing the job it is there for.
The score goes to the console because the screen has no writing on it: there is no way to put text
at a position on the grid, only coloured cells. That is also why the end of a game is the ground
turning DEADGROUND rather than the words GAME OVER, and why a game written on this screen says
what it means with colour.
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 the remainder of a divu 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.
Change .eqv HALFGAP 9 to 6 and play again. A gap is measured outwards from its middle, and both
the drawing and the hit test measure it the same way, so that single number is the entire difficulty
setting of the game.