Flappy bird
The same program in M68K, RISC-V, Z80.
A small Flappy Bird game on a 64 × 64 cell Screen. Tap Space, lowercase w, or Enter to flap. The
bird falls between taps, pipes move left, and passing one adds a point in the Console. A collision
turns the ground dark red; another tap resets the game.
Select Open in editor, then Build and Run. Click the Screen before typing; click it
again after returning to the editor. The fixed testcase below supplies no keys, so it checks the
waiting screen, not flaps or collisions. Screen cells are words in display.
Here is a map for the longer program. Read frame through draw first, then new_pipes,
move_pipes, and hit_test; leave the drawing helpers until you want to see how the picture is
painted.
$s4isREADY(waiting) →PLAYING(moving) →DEAD(falling and waiting for a reset). A flap inDEADjumps tonew_game, returning toREADY; the next flap starts play.$s2is bird height and$s3is vertical speed, both in sixteenths of a row. A smaller row number is higher on the Screen.pipesholds three 12-byte records: left edgexat offset 0, gap middle at offset 4, and a counted flag at offset 8. The initial left edges are 62, 86, and 110.- Rows are 0–63; ground begins at row 56. A bird at top row
topoccupies[top, top + BIRDH), and a pipe atxoccupies[x, x + PIPEW). With gap middlem, the open rows are[m - HALFGAP, m + HALFGAP). The right endpoint is excluded in each interval. - Each frame drains the waiting keys, handles the current state, draws the bird, waits
FRAMEmilliseconds, and repeats. While playing, it updates speed and height, moves pipes, checks collisions, then draws. A reset fromDEADstarts again atnew_gameinstead of finishing that old frame.
# @screen unit=4 width=256 height=256 base=display
.eqv MMIO 0xffff0000
.eqv SIDE 64 # the grid, in words across and down
.eqv GROUNDY 56 # the first row of ground
.eqv GRASSB 58 # and the first row of sand under the grass
.eqv BIRDX 12 # the bird only ever moves up and down
.eqv BIRDW 6
.eqv BIRDH 5
.eqv BIRDR 18 # BIRDX + BIRDW
.eqv PIPEW 8 # and -PIPEW is where a pipe is recycled
.eqv HALFGAP 9
.eqv SPACING 24 # between one pipe and the next
.eqv CYCLE 72 # SPACING * 3, what a recycled pipe jumps by
.eqv FIRSTX 62
.eqv GAPMIN 13 # the highest the middle of a gap goes
.eqv GAPSPAN 26 # and how far below that it can be
.eqv GRAV 3 # sixteenths of a row, per frame, per frame
.eqv FLAPV -26 # the speed one flap gives, upwards
.eqv MAXFALL 26
.eqv STARTY 320 # 20 * 16, where the bird waits
.eqv RESTY 816 # (GROUNDY - BIRDH) * 16, where a dead bird lands
.eqv FRAME 80 # milliseconds a frame
.eqv READY 0
.eqv PLAYING 1
.eqv DEAD 2
.eqv SKY 0x004EC0E8
.eqv PIPE 0x0058BB39
.eqv GRASS 0x0074C458
.eqv SAND 0x00DED895
.eqv DEADGROUND 0x00B03028
.eqv BODY 0x00FACE3E
.eqv BEAK 0x00F08228
.eqv INK 0x00201810
.data
display: .space 16384 # SIDE * SIDE words, four bytes each
pipes: .word 62, 20, 0 # its left edge, the middle of its gap, counted yet
.word 86, 28, 0
.word 110, 16, 0
pipes_end:
seed: .word 0x1F123BB5
label: .asciz "Score: "
over: .asciz "Game over. Score: "
bestmsg: .asciz ", best: "
.text
.globl main
main:
la $s1, display
li $s7, MMIO
li $s6, 0 # the best score of the session
new_game:
li $s2, STARTY # the bird, in sixteenths of a row
li $s3, 0 # and how fast it is falling
li $s4, READY
li $s5, 0 # the score
jal new_pipes
jal paint_grid
frame:
# --- every character typed since the last frame, and only the flaps count ----
li $t8, 0
read_key:
lw $t0, 0($s7) # the receiver control register
andi $t0, $t0, 1 # the Ready bit
beqz $t0, keys_read
lw $t1, 4($s7) # the receiver data, which takes the character
andi $t1, $t1, 0xFF
beq $t1, ' ', flapped
beq $t1, 'w', flapped
bne $t1, 10, read_key # and Enter, which arrives as a newline
flapped:
li $t8, 1 # a flap began this frame
j read_key
keys_read:
beq $s4, PLAYING, playing
beq $s4, DEAD, dead
# --- waiting to start: the bird hangs still until the first flap -------------
beqz $t8, draw
li $v0, 30 # service 30: milliseconds since the run started
syscall
sw $a0, seed # starting time changes the gap sequence
jal new_pipes
jal paint_grid
li $s4, PLAYING
li $s3, FLAPV
j draw
# --- playing -----------------------------------------------------------------
playing:
beqz $t8, fall
li $s3, FLAPV # one tap is one flap, however fast you tap
fall:
addi $s3, $s3, GRAV
ble $s3, MAXFALL, capped
li $s3, MAXFALL
capped:
add $s2, $s2, $s3
bgez $s2, in_sky
li $s2, 0 # the top of the grid is a bump, not a death
li $s3, 0
in_sky:
jal move_pipes
jal hit_test
beqz $v0, draw
li $s4, DEAD
li $a3, DEADGROUND # the ground turns, since there is no text here
jal paint_ground
ble $s5, $s6, say_over
move $s6, $s5
say_over:
li $v0, 4
la $a0, over
syscall
li $v0, 1
move $a0, $s5
syscall
li $v0, 4
la $a0, bestmsg
syscall
li $v0, 1
move $a0, $s6
syscall
li $v0, 11
li $a0, '\n'
syscall
j draw
# --- dead: the bird drops to the ground, then waits for a flap ---------------
dead:
bnez $t8, new_game
addi $s3, $s3, GRAV
ble $s3, MAXFALL, dcapped
li $s3, MAXFALL
dcapped:
add $s2, $s2, $s3
ble $s2, RESTY, draw
li $s2, RESTY
li $s3, 0
draw:
jal draw_bird
li $v0, 32 # service 32: a frame of program time
li $a0, FRAME
syscall
j frame
#-----------------------------------------------------------------------------
# The world
#-----------------------------------------------------------------------------
# new_pipes(): three pipes off to the right, each with a gap of its own
new_pipes:
addi $sp, $sp, -4
sw $ra, 0($sp)
la $t7, pipes
li $t6, FIRSTX
li $t5, 3
np_next:
sw $t6, 0($t7) # where its left edge is
jal random_gap
sw $v0, 4($t7) # the middle of its gap
sw $zero, 8($t7) # and it has not been counted yet
addi $t6, $t6, SPACING
addi $t7, $t7, 12
addi $t5, $t5, -1
bnez $t5, np_next
lw $ra, 0($sp)
addi $sp, $sp, 4
jr $ra
# move_pipes(): everything scrolls one column to the left. A pipe that has left
# the grid jumps a whole cycle to the right with a new gap, which is why three
# pipes are an endless course.
move_pipes:
addi $sp, $sp, -8
sw $ra, 4($sp)
sw $s0, 0($sp)
la $s0, pipes
mp_next:
lw $t7, 0($s0)
addi $t7, $t7, -1
bge $t7, -PIPEW, mp_moved
addi $t7, $t7, CYCLE
jal random_gap
sw $v0, 4($s0)
sw $zero, 8($s0)
mp_moved:
sw $t7, 0($s0)
lw $t6, 8($s0) # already counted?
bnez $t6, mp_counted
addi $t6, $t7, PIPEW
bge $t6, BIRDX, mp_counted
li $t6, 1
sw $t6, 8($s0)
addi $s5, $s5, 1 # its right edge has passed the bird
li $v0, 4
la $a0, label
syscall
li $v0, 1
move $a0, $s5
syscall
li $v0, 11
li $a0, '\n'
syscall
mp_counted:
lw $a0, 0($s0) # the column it has just come into
jal paint_column
lw $a0, 0($s0)
addi $a0, $a0, PIPEW # and the one it has just left
jal paint_column
addi $s0, $s0, 12
la $t7, pipes_end
blt $s0, $t7, mp_next
lw $s0, 0($sp)
lw $ra, 4($sp)
addi $sp, $sp, 8
jr $ra
# hit_test(): 1 in $v0 when the bird is touching a pipe or the ground
hit_test:
srl $t0, $s2, 4 # the bird's top row
addi $t1, $t0, BIRDH # and one past its bottom one
ble $t1, GROUNDY, ht_sky # bottom edge at row 56 is still above ground
li $v0, 1
jr $ra
ht_sky:
la $t2, pipes
li $t3, 3
ht_next:
lw $t4, 0($t2)
bge $t4, BIRDR, ht_skip # this pipe is still to the right of the bird
addi $t5, $t4, PIPEW
ble $t5, BIRDX, ht_skip # and this one is already behind it
lw $t6, 4($t2)
addi $t7, $t6, -HALFGAP
blt $t0, $t7, ht_hit # above the gap
addi $t7, $t6, HALFGAP
bgt $t1, $t7, ht_hit # below it
ht_skip:
addi $t2, $t2, 12
addi $t3, $t3, -1
bnez $t3, ht_next
li $v0, 0
jr $ra
ht_hit:
li $v0, 1
jr $ra
# random_gap(): the middle of the next gap, in $v0, from a 32 bit xorshift.
# It changes $v0, $t9, and HI/LO.
random_gap:
lw $v0, seed
bnez $v0, rg_step # zero would remain zero after every shift and xor
li $v0, 0x1F123BB5
rg_step:
sll $t9, $v0, 13
xor $v0, $v0, $t9 # x = x ^ (x << 13)
srl $t9, $v0, 17
xor $v0, $v0, $t9 # x = x ^ (x >> 17)
sll $t9, $v0, 5
xor $v0, $v0, $t9 # x = x ^ (x << 5)
sw $v0, seed
srl $v0, $v0, 8 # its high bits are the ones worth using
li $t9, GAPSPAN
divu $v0, $t9
mfhi $v0
addi $v0, $v0, GAPMIN
jr $ra
#-----------------------------------------------------------------------------
# Drawing
#-----------------------------------------------------------------------------
# fill_span(col, from, to, colour): rows from to to-1 of one column, in one
# colour, and never a row outside the window in $t8 and $t9. It also changes
# $a1 and $a2 when clipping, plus $t0 and $t1.
fill_span:
bge $a1, $t8, fs_top
move $a1, $t8
fs_top:
ble $a2, $t9, fs_rows
move $a2, $t9
fs_rows:
sub $t1, $a2, $a1
blez $t1, fs_done # a span the window has closed draws nothing
sll $t0, $a1, 6 # row * SIDE
add $t0, $t0, $a0 # + col
sll $t0, $t0, 2 # four bytes a word
add $t0, $t0, $s1
fs_next:
sw $a3, 0($t0)
addi $t0, $t0, 256 # SIDE * 4, straight down to the next row
addi $t1, $t1, -1
bnez $t1, fs_next
fs_done:
jr $ra
# world_column(col): the sky, the pipe standing in this column if one does, and
# the ground, in as much of the column as the window allows. A column off either
# side of the grid draws nothing.
world_column:
bltz $a0, wc_done
bge $a0, SIDE, wc_done
addi $sp, $sp, -4
sw $ra, 0($sp)
la $t2, pipes # is a pipe standing in this column
li $t3, 3
li $t4, -1
wc_find:
lw $t5, 0($t2)
blt $a0, $t5, wc_skip
addi $t6, $t5, PIPEW
bge $a0, $t6, wc_skip
lw $t4, 4($t2) # the middle of its gap
wc_skip:
addi $t2, $t2, 12
addi $t3, $t3, -1
bnez $t3, wc_find
bltz $t4, wc_sky
li $a3, PIPE
li $a1, 0
addi $a2, $t4, -HALFGAP
jal fill_span # from the top of the grid down to the gap
li $a3, SKY
addi $a1, $t4, -HALFGAP
addi $a2, $t4, HALFGAP
jal fill_span # the gap itself
li $a3, PIPE
addi $a1, $t4, HALFGAP
li $a2, GROUNDY
jal fill_span # and from the gap down to the ground
j wc_ground
wc_sky:
li $a3, SKY
li $a1, 0
li $a2, GROUNDY
jal fill_span
wc_ground:
li $a3, GRASS
li $a1, GROUNDY
li $a2, GRASSB
jal fill_span
li $a3, SAND
li $a1, GRASSB
li $a2, SIDE
jal fill_span
lw $ra, 0($sp)
addi $sp, $sp, 4
wc_done:
jr $ra
# paint_column(col): the whole of one column of the world
paint_column:
li $t8, 0
li $t9, SIDE
j world_column # which returns to paint_column's own caller
# paint_grid(): every column, which is the one full repaint a game has
paint_grid:
addi $sp, $sp, -8
sw $ra, 4($sp)
sw $s0, 0($sp)
li $s0, 0
pg_next:
move $a0, $s0
jal paint_column
addi $s0, $s0, 1
blt $s0, SIDE, pg_next
lw $s0, 0($sp)
lw $ra, 4($sp)
addi $sp, $sp, 8
jr $ra
# paint_ground(colour): the eight rows of ground, right across the grid
paint_ground:
addi $sp, $sp, -4
sw $ra, 0($sp)
li $t8, 0
li $t9, SIDE
li $a0, 0
pd_next:
li $a1, GROUNDY
li $a2, SIDE
jal fill_span
addi $a0, $a0, 1
blt $a0, SIDE, pd_next
lw $ra, 0($sp)
addi $sp, $sp, 4
jr $ra
# draw_bird(): in each bird column, paint the world above, the body, then the
# world below. The eye and beak then replace a few body cells.
draw_bird:
addi $sp, $sp, -8
sw $ra, 4($sp)
sw $s0, 0($sp)
srl $t7, $s2, 4 # the bird's top row, which world_column leaves alone
li $s0, BIRDX
db_next:
move $a0, $s0
li $t8, 0
move $t9, $t7
jal world_column # the world above the bird
move $a0, $s0
li $t8, 0
li $t9, SIDE
li $a3, BODY
move $a1, $t7
addi $a2, $t7, BIRDH
jal fill_span # the bird itself
move $a0, $s0
addi $t8, $t7, BIRDH
li $t9, SIDE
jal world_column # and the world below it
addi $s0, $s0, 1
blt $s0, BIRDR, db_next
li $t8, 0
li $t9, SIDE
li $a3, INK # an eye
li $a0, 16
addi $a1, $t7, 1
addi $a2, $t7, 2
jal fill_span
li $a3, BEAK # and a beak, over the body's last column
li $a0, 17
addi $a1, $t7, 2
addi $a2, $t7, 4
jal fill_span
lw $s0, 0($sp)
lw $ra, 4($sp)
addi $sp, $sp, 8
jr $ra
Follow one frame
read_key checks the keyboard receiver's Ready bit. Reading its data register consumes one
character, so the loop keeps reading until the queue is empty. Space, lowercase w, and Enter
(newline 10) set $t8 to 1. Several flap characters collected during one frame still make one
flap. In READY, that flap initializes the pipes and sets speed to FLAPV; movement begins on the
next PLAYING frame. In DEAD, a flap jumps to new_game, which waits for another flap.
During PLAYING, a flap sets speed $s3 to -26. Gravity then adds 3, the speed is limited to
MAXFALL, and that speed is added to height $s2. Starting at STARTY = 320 (row 20), the first
playing frame after a flap makes speed -26 + 3 = -23 and height 320 - 23 = 297. The drawn top
row is 297 >> 4 = 18. With no flap on the next frame, speed becomes -20, height becomes 277,
and the drawn top row is 17. The lower four bits of height remain stored, so small changes can
accumulate even when the drawn row stays the same. Reaching the top clamps height and speed to 0.
move_pipes moves each left edge one column left. Once an edge moves past -PIPEW (-8), it adds
CYCLE (72), chooses a new gap, and clears that record's counted flag. Since there are three
pipes spaced 24 columns apart, CYCLE = 3 × SPACING keeps their spacing. When a pipe's right edge
is strictly left of BIRDX (12), its flag changes to 1 and the Console prints the new score.
hit_test compares edges using the half-open intervals in the map. For example, with gap middle
20 and HALFGAP = 9, open rows are [11, 29), meaning rows 11 through 28. A bird with top row
24 occupies [24, 29) and fits vertically; top row 25 occupies [25, 30) and touches the lower
pipe if their columns overlap. Ground begins at row 56, so a bird occupying [51, 56) is still
clear, while [52, 57) touches it. A collision changes $s4 to DEAD, paints the ground red,
and prints the final and best scores. The dead bird then falls to its resting height.
Read the drawing helpers
world_column computes sky, pipe, gap, grass, and sand from a column number and the three pipe
records. fill_span paints rows in [from, to) within the clipping window $t8 to $t9. When
it clips a span it changes $a1 or $a2; it also uses $t0 and $t1. For each moving pipe,
move_pipes repaints the column it entered and the column it left. paint_grid paints the full
Screen at the start of a game.
draw_bird repaints its six columns in three bands: world above, yellow body, world below. It
then puts the eye and beak over some body cells, so those cells receive another write. Painting
these bands avoids a separate erase pass for the bird. Screen updates can still be visible while
they happen; the code does not promise flicker-free display.
The screen contains coloured cells, not text. Scores therefore go to the Console, while red
ground marks a collision on the Screen. new_pipes, move_pipes, paint_grid, and draw_bird
save $ra because they call other routines; the loops that use $s0 save and restore it too.
random_gap changes $v0 and $t9, as well as the division's HI and LO registers. That leaves
the pipe's left edge in $t7 available across the call.
random_gap advances a stored number with shifts and xor, then divides by GAPSPAN to obtain
a remainder from 0 through 25. Adding GAPMIN gives a middle row from 13 through 38. Service 30
supplies a starting time on the first flap, which changes this repeatable sequence according to
when play starts. If that time is zero, the generator uses its nonzero fallback seed; zero would
stay zero through every xorshift step.
Try it
Change .eqv HALFGAP 9 to 6. Before running, predict the open rows for a pipe whose middle is
20: [20 - 6, 20 + 6) = [14, 26), or rows 14 through 25. Build and Run to see narrower gaps.
Both drawing and collision checking use the same constant, so a bird at top row 22 occupies
[22, 27) and no longer fits in that example gap.
Restore HALFGAP to 9, then change .eqv FLAPV -26 to -16. Predict the first playing frame
after a starting flap: speed becomes -16 + 3 = -13, height becomes 320 - 13 = 307, and the
drawn top row is 307 >> 4 = 19. Build and Run, tap once, and watch for a gentler rise. The
starting flap still draws the waiting bird at row 20 before that playing frame.