Flappy bird
The same program in MIPS, 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, 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. You can also click on the drawing itself, since the mouse counts as a flap too.
Play it first, then come back to the listing. It is eight hundred lines and most of them are drawing, so do not read it top to bottom. The banners inside it split it into four parts, one frame, input, the world and drawing, and the sections under the listing take the four ideas that are worth having in their own right.
Three states, one frame loop
A game is always in one of three states, and state says which: READY while the bird hangs still
waiting for the first flap, PLAYING, and DEAD while it drops to the ground.
frame:
bsr readflap
move.w d0,d7
move.w state,d0
cmp.w #PLAYING,d0
beq playing
cmp.w #DEAD,d0
beq dead
Every frame reads the input once, branches on state, and ends at draw whichever way it went, so
one pass round that loop is exactly one frame. The three states differ only in what they do to the
bird and the pipes on the way through. That shape is what keeps a game readable at this size: there
is one place where a frame begins and one place where it ends, and adding a fourth state means
adding a branch and a label, not rethinking anything.
d7 carries one thing across the whole loop, which is whether a flap began this frame. Nothing the
loop calls is allowed to touch it, and that is written in the comment above frame because nothing
in the machine will enforce it.
A press, not a key held down
readflap has to answer "did a flap begin" and the tasks it has only answer "is something down right
now". A key held for twenty frames reads as pressed twenty times, which would be twenty flaps.
clr.w held
moveq #0,d0
rts
pressed:
tst.w held
bne stillheld
move.w #1,held
moveq #1,d0 ; this frame is where the press began
rts
held is one word of memory remembering the answer from last time, and a press only counts when the
frame before it had nothing down. Any program polling anything needs this the moment it cares about
edges rather than levels.
Sixteenths of a pixel
birdy is a long and it does not count pixels, it counts sixteenths of one. Gravity adds GRAV,
which is 5, to birdv every frame, and a flap sets birdv to -80.
In whole pixels those two numbers would be 0 and -5, so the bird would either not accelerate at all
or drop like a brick, and there is nothing in between to pick. Sixteen times finer gives sixteen
times as many speeds to choose from, out of the same integer arithmetic. asr.l #4,d0 turns the
sixteenths back into the pixel the bird is drawn at, and the four bits it shifts away are the
fractional part the next frame carries on with.
Three pipes and an endless course
movepipes slides each pipe left by SPEED. A pipe whose left edge has gone past NEGPIPE, one
pipe width off the left of the screen, jumps CYCLE to the right and asks randgap for a new gap.
CYCLE is SPACING*3, so the recycled pipe lands exactly where a fourth pipe would have been and
the spacing never drifts, however long you play. Three pipes is all the memory the course ever needs.
The third word of a pipe's record is whether it has been counted, and it puts one on the score when
the pipe's right edge passes BIRDX.
The gaps come out of arithmetic rather than out of anywhere random:
randgap:
moveq #0,d0
move.w seed,d0
mulu #25173,d0
add.l #13849,d0
move.w d0,seed ; the low word of the product is the next seed
Multiply the seed, add a constant, keep the low word, and the sequence runs a long way before it
repeats. The high byte is the half worth using, and mulu #GAPSPAN then lsr.l #8 squeezes a byte
into a number from 0 to GAPSPAN with no division anywhere. The seed itself is read from task 8 on
the frame the first flap happens, so the course depends on when you started playing rather than on a
number written into the program.
The rest of it
hittest measures a box HITIN pixels smaller than the bird on every side, which is what makes a
near miss feel like a near miss instead of a hit. It also skips any pipe still to the right of the
bird or already behind it, so on most frames the whole collision test is two comparisons per pipe.
The drawing is the double buffering from A bouncing ball: task 92 mode 17 at the start, the frame
painted back to front into the off screen image, task 94 to show all of it at once. Task 23 then lets
DELAY hundredths of a second of program time pass, so the bird falls at the same rate whatever
machine is underneath. The clouds, the tufts of grass and the wing that beats through three positions
cost nothing but the order they are drawn in.
The score and the messages go through task 95, which puts text at a pixel position rather than at the
text cursor. The screen's font is an 8 by 16 cell, so ctext counts the characters, takes four
pixels off the centre for each one, and draws from there. appnum builds the digits backwards into a
buffer, because a division gives you the last digit first.
GAPH is the whole of the difficulty, and HALFGAP under it has to change with it: the drawing
measures the gap from its middle, so those two names are one number written twice.