Flappy bird

Build a small game in which a tap lifts the bird and gravity pulls it back down. Pipes scroll in from the right. Passing one earns a point; touching a pipe or the ground ends the round.

Choose Build, then Run. Click inside the Screen so it has keyboard focus. Tap Space, W, Enter or the up arrow to start and flap; clicking the Screen also flaps. Release between taps: holding a key or mouse button gives only one flap. After a collision, tap again to restart. Choose Stop when you are done.

The listing is long because it draws the scene as well as running the game. Use this map before reading it:

Follow this partWhat it does
frame → readflapFind out whether a new tap began.
READY, playing, deadStart a round, advance it, or let the bird land.
movepipes → hittestMove the obstacles, award points and check collisions during play.
drawDraw the scene, show it with task 94, then pause with task 23.

Start with frame, readflap, movepipes and hittest. You can skim the shape and colour helpers under Drawing on a first pass.

    ORG     $1000

start:
    move.b  #92,d0
    move.b  #17,d1
    trap    #15                 ; mode 17: draw off screen until task 94

    move.b  #93,d0
    move.b  #1,d1
    trap    #15                 ; a one pixel pen for every outline

newgame:
    bsr     reset

* --- one frame ---------------------------------------------------------------
* d7 carries "a flap began this frame" from the input read to the state machine,
* so nothing called from here is allowed to touch it.
frame:
    bsr     readflap
    move.w  d0,d7

    move.w  state,d0
    cmp.w   #PLAYING,d0
    beq     playing
    cmp.w   #DEAD,d0
    beq     dead

* --- waiting to start: the bird hangs still until the first flap --------------
    tst.w   d7
    beq     draw
    move.b  #8,d0
    trap    #15                 ; hundredths of a second since the run started
    move.w  d1,seed             ; so the course depends on when the player began
    bsr     newpipes
    move.w  #PLAYING,state
    move.w  #FLAPV,birdv
    bra     draw

* --- playing -----------------------------------------------------------------
playing:
    tst.w   d7
    beq     fall
    move.w  #FLAPV,birdv        ; one tap is one flap, however long the key is held
    clr.w   wing                ; and the wing beat starts again with it
fall:
    move.w  birdv,d0
    add.w   #GRAV,d0
    cmp.w   #MAXFALL,d0
    ble     capped
    move.w  #MAXFALL,d0
capped:
    move.w  d0,birdv
    ext.l   d0
    add.l   d0,birdy            ; birdy counts sixteenths of a pixel

    tst.l   birdy
    bge     insky
    clr.l   birdy               ; the top of the screen is a bump, not a death
    clr.w   birdv
insky:
    bsr     movepipes
    bsr     hittest
    tst.w   d0
    beq     draw

    move.w  #DEAD,state
    move.w  points,d0
    cmp.w   record,d0
    ble     draw
    move.w  d0,record
    bra     draw

* --- dead: the bird drops to the ground, then waits for a flap ----------------
dead:
    move.w  birdv,d0
    add.w   #GRAV,d0
    cmp.w   #MAXFALL,d0
    ble     dcapped
    move.w  #MAXFALL,d0
dcapped:
    move.w  d0,birdv
    ext.l   d0
    add.l   d0,birdy

    move.l  birdy,d0
    asr.l   #4,d0
    cmp.w   #RESTY,d0
    blt     dfalling
    move.l  #RESTY16,birdy
    clr.w   birdv
dfalling:
    tst.w   d7
    beq     draw
    bra     newgame

* --- the frame is painted back to front, then shown all at once ---------------
draw:
    bsr     drawsky
    bsr     drawpipes
    bsr     drawground
    bsr     drawbird
    bsr     drawhud

    move.b  #94,d0
    trap    #15                 ; the finished frame appears here, all at once

    move.b  #23,d0
    move.l  #DELAY,d1
    trap    #15                 ; wait at least DELAY hundredths before the next frame
    bra     frame

*-----------------------------------------------------------------------------
* Input
*-----------------------------------------------------------------------------
* Answers d0.w = 1 on the frame a flap begins. Both tasks report what is held
* right now, so the "began" part is this program's: a press only counts when
* the frame before it had nothing pressed.
readflap:
    move.b  #19,d0
    move.l  #KEYLIST,d1
    trap    #15                 ; one $FF/$00 byte per key, in the same order
    move.l  d1,d2

    btst    #24,d2              ; space
    bne     pressed
    btst    #16,d2              ; the up arrow
    bne     pressed
    btst    #8,d2               ; w
    bne     pressed
    btst    #0,d2               ; enter
    bne     pressed

    move.b  #61,d0
    move.b  #0,d1
    trap    #15                 ; the mouse as it is right now
    btst    #0,d0               ; bit 0 of the flags byte is the left button
    bne     pressed

    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
stillheld:
    moveq   #0,d0
    rts

*-----------------------------------------------------------------------------
* The world
*-----------------------------------------------------------------------------
* A new game, keeping the best score of the session.
reset:
    move.w  #READY,state
    move.l  #STARTY16,birdy
    clr.w   birdv
    clr.w   points
    clr.w   wing
    clr.w   scroll
    bsr     newpipes
    rts

* Three pipes, evenly spaced off to the right, each with a gap of its own.
newpipes:
    lea     pipes,a0
    move.w  #FIRSTX,d2
    moveq   #2,d3
npnext:
    move.w  d2,(a0)+            ; where its left edge is
    bsr     randgap
    move.w  d0,(a0)+            ; the middle of its gap
    clr.w   (a0)+               ; and it has not been counted yet
    add.w   #SPACING,d2
    dbra    d3,npnext
    rts

* The middle of the next gap, from a sixteen bit congruential generator: one
* seed always lays out the same course, and the seed comes from the clock.
* Answers in d0.w and leaves every other register alone.
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
    moveq   #0,d0
    move.w  seed,d0
    lsr.l   #8,d0               ; its high byte is the half worth using
    mulu    #GAPSPAN,d0
    lsr.l   #8,d0               ; 0 to GAPSPAN-1, without dividing
    add.w   #GAPMIN,d0
    rts

* Everything scrolls left. A pipe that has left the screen jumps a whole cycle
* to the right with a new gap, which is why three pipes are an endless course.
movepipes:
    move.w  scroll,d0
    sub.w   #SPEED,d0
    bge     scrolled
    add.w   #TUFTGAP,d0         ; the tufts repeat, so this wrap is invisible
scrolled:
    move.w  d0,scroll

    lea     pipes,a0
    moveq   #2,d3
mpnext:
    move.w  (a0),d1
    sub.w   #SPEED,d1
    cmp.w   #NEGPIPE,d1
    bgt     mpmoved
    add.w   #CYCLE,d1
    bsr     randgap
    move.w  d0,2(a0)
    clr.w   4(a0)
mpmoved:
    move.w  d1,(a0)

    tst.w   4(a0)               ; already counted?
    bne     mpdone
    move.w  d1,d2
    add.w   #PIPEW,d2
    cmp.w   #BIRDX,d2
    bge     mpdone
    move.w  #1,4(a0)
    addq.w  #1,points           ; its right edge has passed the bird
mpdone:
    lea     6(a0),a0
    dbra    d3,mpnext
    rts

* Answers d0.w = 1 when the bird touches a pipe or the ground. Only the pipe
* test uses an inset box; the ground checks the bird's full height.
hittest:
    move.l  birdy,d1
    asr.l   #4,d1
    move.w  d1,d2
    add.w   #BIRDH,d2
    cmp.w   #GROUNDY,d2
    bge     hit                 ; the ground is met by the whole bird

    add.w   #HITIN,d1           ; the top of the box the pipes are tested with
    sub.w   #HITIN,d2           ; and its bottom

    lea     pipes,a0
    moveq   #2,d3
htnext:
    move.w  (a0),d4
    move.w  d4,d5
    add.w   #PIPEW,d5
    cmp.w   #HITR,d4
    bge     htskip              ; this pipe is still to the right of the bird
    cmp.w   #HITL,d5
    ble     htskip              ; and this one is already behind it

    move.w  2(a0),d6
    sub.w   #HALFGAP,d6
    cmp.w   d6,d1
    blt     hit                 ; above the gap
    add.w   #GAPH,d6
    cmp.w   d6,d2
    bgt     hit                 ; below it
htskip:
    lea     6(a0),a0
    dbra    d3,htnext

    moveq   #0,d0
    rts
hit:
    moveq   #1,d0
    rts

*-----------------------------------------------------------------------------
* Drawing
*-----------------------------------------------------------------------------
* The sky, and two clouds drifting across it.
drawsky:
    move.l  #SKY,d1
    move.b  #80,d0
    trap    #15
    move.l  #SKY,d1
    move.b  #81,d0
    trap    #15
    moveq   #0,d1
    moveq   #0,d2
    move.w  #WIDTH,d3
    move.w  #HEIGHT,d4
    move.b  #87,d0
    trap    #15

    move.l  #CLOUDC,d1
    move.b  #80,d0
    trap    #15
    move.l  #CLOUDC,d1
    move.b  #81,d0
    trap    #15                 ; pen and fill alike, so the lobes have no seams

    move.w  cloud1x,d5
    subq.w  #1,d5
    cmp.w   #NEGCLOUD,d5
    bgt     cl1ok
    move.w  #WIDTH,d5           ; gone on the left, back in on the right
cl1ok:
    move.w  d5,cloud1x
    move.w  #56,d6
    bsr     cloud

    move.w  cloud2x,d5
    subq.w  #1,d5
    cmp.w   #NEGCLOUD,d5
    bgt     cl2ok
    move.w  #WIDTH,d5
cl2ok:
    move.w  d5,cloud2x
    move.w  #120,d6
    bsr     cloud
    rts

* One cloud: three overlapping discs, at d5.w across and d6.w down.
cloud:
    move.w  d5,d1
    move.w  d6,d2
    move.w  d1,d3
    add.w   #70,d3
    move.w  d2,d4
    add.w   #34,d4
    move.b  #88,d0
    trap    #15

    move.w  d5,d1
    add.w   #40,d1
    move.w  d6,d2
    sub.w   #16,d2
    move.w  d1,d3
    add.w   #60,d3
    move.w  d2,d4
    add.w   #50,d4
    move.b  #88,d0
    trap    #15

    move.w  d5,d1
    add.w   #86,d1
    move.w  d6,d2
    add.w   #6,d2
    move.w  d1,d3
    add.w   #54,d3
    move.w  d2,d4
    add.w   #28,d4
    move.b  #88,d0
    trap    #15
    rts

* The three pipes. a0 walks the table, one pipe per call.
drawpipes:
    move.l  #PIPEDK,d1
    move.b  #80,d0
    trap    #15
    move.l  #PIPEC,d1
    move.b  #81,d0
    trap    #15

    lea     pipes,a0
    bsr     onepipe
    bsr     onepipe
    bsr     onepipe
    rts

* A column down from the top and one up from the ground, each capped with a
* wider lip, then a0 moves on to the pipe after it.
onepipe:
    move.w  (a0),d5             ; its left edge
    move.w  2(a0),d6            ; and the middle of its gap

    move.w  d5,d1
    moveq   #0,d2
    move.w  d5,d3
    add.w   #PIPEW,d3
    move.w  d6,d4
    sub.w   #HALFGAP,d4         ; down to the top of the gap
    move.b  #87,d0
    trap    #15

    move.w  d5,d1
    sub.w   #LIPOUT,d1
    move.w  d4,d2
    sub.w   #LIPH,d2
    move.w  d5,d3
    add.w   #PIPER,d3
    move.b  #87,d0
    trap    #15                ; the lip, still ending at the gap

    move.w  d5,d1
    move.w  d6,d2
    add.w   #HALFGAP,d2         ; from the bottom of the gap
    move.w  d5,d3
    add.w   #PIPEW,d3
    move.w  #GROUNDY,d4
    move.b  #87,d0
    trap    #15

    move.w  d5,d1
    sub.w   #LIPOUT,d1
    move.w  d6,d2
    add.w   #HALFGAP,d2
    move.w  d5,d3
    add.w   #PIPER,d3
    move.w  d2,d4
    add.w   #LIPH,d4
    move.b  #87,d0
    trap    #15

    lea     6(a0),a0
    rts

* Sand, a band of grass on top of it, and tufts that scroll with the pipes.
drawground:
    move.l  #SAND,d1
    move.b  #80,d0
    trap    #15
    move.l  #SAND,d1
    move.b  #81,d0
    trap    #15
    moveq   #0,d1
    move.w  #GROUNDY,d2
    move.w  #WIDTH,d3
    move.w  #HEIGHT,d4
    move.b  #87,d0
    trap    #15

    move.l  #GRASS,d1
    move.b  #80,d0
    trap    #15
    move.l  #GRASS,d1
    move.b  #81,d0
    trap    #15
    moveq   #0,d1
    move.w  #GROUNDY,d2
    move.w  #WIDTH,d3
    move.w  #GRASSB,d4
    move.b  #87,d0
    trap    #15

    move.l  #GRASSDK,d1
    move.b  #80,d0
    trap    #15
    move.l  #GRASSDK,d1
    move.b  #81,d0
    trap    #15

* one tuft further left than the screen, so the row never runs out on that side
    move.w  scroll,d5
    sub.w   #TUFTGAP,d5
    moveq   #11,d6
gtnext:
    move.w  d5,d1
    move.w  #TUFTY,d2
    move.w  d5,d3
    add.w   #TUFTW,d3
    move.w  #TUFTB,d4
    move.b  #87,d0
    trap    #15
    add.w   #TUFTGAP,d5
    dbra    d6,gtnext
    rts

* The bird: a body, a wing that beats through three positions, an eye and a
* beak. Only the body's box is what `hittest` measures.
drawbird:
    move.l  birdy,d5
    asr.l   #4,d5               ; sixteenths of a pixel back down to pixels

    move.l  #INK,d1
    move.b  #80,d0
    trap    #15                 ; one dark outline for all of it

    move.l  #BODY,d1
    move.b  #81,d0
    trap    #15
    move.w  #BIRDX,d1
    move.w  d5,d2
    move.w  #BIRDR,d3
    move.w  d5,d4
    add.w   #BIRDH,d4
    move.b  #88,d0
    trap    #15

    moveq   #0,d6
    move.w  wing,d6
    move.w  state,d0
    cmp.w   #DEAD,d0
    beq     wingset             ; a dead bird stops beating
    addq.w  #1,d6
    cmp.w   #12,d6
    blt     wingheld
    moveq   #0,d6
wingheld:
    move.w  d6,wing
wingset:
    lsr.w   #2,d6               ; twelve frames, three positions
    mulu    #7,d6
    add.w   #WINGTOP,d6

    move.l  #WINGC,d1
    move.b  #81,d0
    trap    #15
    move.w  #WINGL,d1
    move.w  d5,d2
    add.w   d6,d2
    move.w  #WINGR,d3
    move.w  d2,d4
    add.w   #13,d4
    move.b  #88,d0
    trap    #15

    move.l  #PAPER,d1
    move.b  #81,d0
    trap    #15
    move.w  #EYEL,d1
    move.w  d5,d2
    add.w   #7,d2
    move.w  #EYER,d3
    move.w  d2,d4
    add.w   #14,d4
    move.b  #88,d0
    trap    #15

    move.l  #INK,d1
    move.b  #81,d0
    trap    #15
    move.w  #PUPL,d1
    move.w  d5,d2
    add.w   #11,d2
    move.w  #PUPR,d3
    move.w  d2,d4
    add.w   #7,d4
    move.b  #88,d0
    trap    #15

    move.l  #BEAKC,d1
    move.b  #81,d0
    trap    #15
    move.w  #BEAKL,d1
    move.w  d5,d2
    add.w   #19,d2
    move.w  #BEAKR,d3
    move.w  d2,d4
    add.w   #9,d4
    move.b  #87,d0
    trap    #15
    rts

* The score, and whatever state the game is in has to say.
drawhud:
    move.w  state,d0
    cmp.w   #READY,d0
    beq     hudready

    move.l  #INK,d1
    move.b  #81,d0
    trap    #15
    move.l  #PAPER,d1
    move.b  #80,d0
    trap    #15
    move.w  #PLATEL,d1
    move.w  #14,d2
    move.w  #PLATER,d3
    move.w  #46,d4
    move.b  #87,d0
    trap    #15

    lea     line,a1
    moveq   #0,d1
    move.w  points,d1
    bsr     appnum
    lea     line,a1
    move.w  #22,d6
    bsr     ctext

    move.w  state,d0
    cmp.w   #DEAD,d0
    beq     huddead
    rts

hudready:
    move.w  #250,d1
    move.w  #392,d2
    bsr     plate

    lea     title,a1
    move.w  #266,d6
    bsr     ctext
    lea     hint1,a1
    move.w  #302,d6
    bsr     ctext
    lea     hint2,a1
    move.w  #326,d6
    bsr     ctext
    lea     hint3,a1
    move.w  #358,d6
    bsr     ctext
    rts

huddead:
    move.w  #150,d1
    move.w  #330,d2
    bsr     plate

    lea     overmsg,a1
    move.w  #172,d6
    bsr     ctext

    lea     line,a1
    lea     pointsmsg,a0
    bsr     append
    moveq   #0,d1
    move.w  points,d1
    bsr     appnum
    lea     line,a1
    move.w  #210,d6
    bsr     ctext

    lea     line,a1
    lea     recordmsg,a0
    bsr     append
    moveq   #0,d1
    move.w  record,d1
    bsr     appnum
    lea     line,a1
    move.w  #238,d6
    bsr     ctext

    lea     againmsg,a1
    move.w  #286,d6
    bsr     ctext
    rts

* A plate across the middle of the screen, from d1.w down to d2.w.
plate:
    move.w  d1,d5
    move.w  d2,d6
    move.l  #INK,d1
    move.b  #81,d0
    trap    #15
    move.l  #PAPER,d1
    move.b  #80,d0
    trap    #15
    move.w  #PANELL,d1
    move.w  d5,d2
    move.w  #PANELR,d3
    move.w  d6,d4
    move.b  #87,d0
    trap    #15
    rts

*-----------------------------------------------------------------------------
* Text
*-----------------------------------------------------------------------------
* Draws the NULL terminated string at (a1) centred across the screen with its
* top at d6.w. The screen's font is an 8 by 16 cell, so half a character is the
* four pixels each one takes off the centre.
ctext:
    movea.l a1,a0
    moveq   #0,d5
ctlen:
    move.b  (a0)+,d0
    beq     ctdraw
    addq.w  #1,d5
    bra     ctlen
ctdraw:
    asl.w   #2,d5
    move.w  #CENTREX,d1
    sub.w   d5,d1
    move.w  d6,d2
    move.b  #95,d0
    trap    #15
    rts

* Appends the NULL terminated string at (a0) to the one at (a1), leaving a1 on
* the new terminator so the next call carries straight on from there.
append:
    move.b  (a0)+,d0
    beq     appdone
    move.b  d0,(a1)+
    bra     append
appdone:
    clr.b   (a1)
    rts

* Appends d1.l, a number up to 65535, as decimal at (a1). Digits come out least
* significant first, so they are written backwards into a scratch buffer and
* the whole thing is appended at the end.
appnum:
    lea     numend,a0
    clr.b   -(a0)
annext:
    divu    #10,d1
    move.l  d1,d2
    swap    d2                  ; the remainder is this digit
    add.w   #$30,d2
    move.b  d2,-(a0)
    andi.l  #$FFFF,d1           ; and the quotient is what is left to do
    bne     annext
    bra     append

*-----------------------------------------------------------------------------
* State
*-----------------------------------------------------------------------------
state:      dc.w    0
birdy:      dc.l    STARTY16    ; sixteenths of a pixel, so gravity is smooth
birdv:      dc.w    0
points:     dc.w    0
record:     dc.w    0
wing:       dc.w    0
held:       dc.w    0           ; was a flap key or button down last frame?
scroll:     dc.w    0
cloud1x:    dc.w    70
cloud2x:    dc.w    390
seed:       dc.w    $1D4B

* one pipe is its left edge, the middle of its gap, and whether it is counted
pipes:      dc.w    0,0,0
            dc.w    0,0,0
            dc.w    0,0,0

title:      dc.b    'FLAPPY 68K',0
hint1:      dc.b    'TAP SPACE, W, ENTER OR THE UP ARROW',0
hint2:      dc.b    'OR CLICK ON THE SCREEN ITSELF',0
hint3:      dc.b    'CLICK THE SCREEN PANEL SO IT TAKES THE KEYS',0
overmsg:    dc.b    'GAME OVER',0
pointsmsg:  dc.b    'SCORE ',0
recordmsg:  dc.b    'BEST ',0
againmsg:   dc.b    'FLAP TO PLAY AGAIN',0

line:       ds.b    48
numbuf:     ds.b    8
numend:     ds.b    1

*-----------------------------------------------------------------------------
* Tunables
*-----------------------------------------------------------------------------
WIDTH   equ     640
HEIGHT  equ     480
CENTREX equ     320
GROUNDY equ     416             ; the top of the ground
GRASSH  equ     10
GRASSB  equ     426             ; GROUNDY+GRASSH
TUFTY   equ     410
TUFTB   equ     418
TUFTW   equ     20
TUFTGAP equ     56
CLOUDW  equ     140
NEGCLOUD equ    -140            ; -CLOUDW, where a cloud comes back on the right

READY   equ     0
PLAYING equ     1
DEAD    equ     2

BIRDX   equ     150             ; the bird only ever moves up and down
BIRDW   equ     46
BIRDH   equ     34
BIRDR   equ     196             ; BIRDX+BIRDW
HITIN   equ     6               ; the pipe hit box is smaller by this much on each side
HITL    equ     156             ; BIRDX+HITIN
HITR    equ     190             ; BIRDX+BIRDW-HITIN
STARTY  equ     196
STARTY16 equ    3136            ; STARTY*16
RESTY   equ     382             ; GROUNDY-BIRDH, where a dead bird lands
RESTY16 equ     6112            ; RESTY*16

GRAV    equ     5               ; sixteenths of a pixel, per frame, per frame
FLAPV   equ     -80             ; the speed one flap gives, upwards
MAXFALL equ     150

WINGTOP equ     5
WINGL   equ     159             ; BIRDX+9
WINGR   equ     183             ; BIRDX+33
EYEL    equ     174             ; BIRDX+24
EYER    equ     188             ; BIRDX+38
PUPL    equ     180             ; BIRDX+30
PUPR    equ     187             ; BIRDX+37
BEAKL   equ     190             ; BIRDX+40
BEAKR   equ     208             ; BIRDX+58

PIPEW   equ     70
PIPER   equ     76              ; PIPEW+LIPOUT
NEGPIPE equ     -70             ; -PIPEW, where a pipe is recycled
LIPH    equ     18
LIPOUT  equ     6
GAPH    equ     140
HALFGAP equ     70
SPEED   equ     4               ; pixels a frame, for everything that scrolls
SPACING equ     240             ; between one pipe and the next
CYCLE   equ     720             ; SPACING*3, what a recycled pipe jumps by
FIRSTX  equ     620
GAPMIN  equ     120             ; the highest the middle of a gap goes
GAPSPAN equ     180             ; the offset ranges from 0 through 179

DELAY   equ     3               ; hundredths of a second a frame
KEYLIST equ     $2026570D       ; space, up arrow, w, enter

PLATEL  equ     284
PLATER  equ     356
PANELL  equ     104
PANELR  equ     536

* colors are $00BBGGRR longs: blue high, then green, then red
SKY     equ     $00E8C04E
CLOUDC  equ     $00F8FBFF
PIPEC   equ     $0039BB58
PIPEDK  equ     $00347A29
SAND    equ     $0095D8DE
GRASS   equ     $0058C474
GRASSDK equ     $0040A04C
BODY    equ     $003ECEFA
WINGC   equ     $00D2F5FF
BEAKC   equ     $002882F0
INK     equ     $00101820
PAPER   equ     $00FFFFFF

Follow one frame

state tells frame which part of the game to run. In READY, the bird waits for the first flap. That flap seeds a new set of pipes, sets the upward speed and changes state to PLAYING. On later frames, playing applies gravity, moves the pipes and checks for a hit. A hit changes state to DEAD; the bird then falls to the ground and waits for another flap. Every path reaches draw, which shows the completed picture and loops back to frame.

frame:
    bsr     readflap
    move.w  d0,d7

    move.w  state,d0
    cmp.w   #PLAYING,d0
    beq     playing
    cmp.w   #DEAD,d0
    beq     dead

readflap returns 1 in d0 when a new flap begins. The loop copies that answer to d7 before using d0 to inspect state. The routines called later leave d7 alone, so the state code can still test the flap after those calls.

Turn a held input into one flap

Task 19 reports which keys are down, and task 61 reports whether the mouse button is down. Both describe the current state. If a key stays down for several frames, readflap must return 1 only on the first of them. The word held remembers whether any flap input was down on the previous read:

    clr.w   held
    moveq   #0,d0
    rts
pressed:
    tst.w   held
    bne     stillheld
    move.w  #1,held
    moveq   #1,d0
    rts

When all inputs are released, held becomes 0. On the next pressed read, the code sets held to 1 and returns a flap. Further reads while it stays pressed return 0. This is why you release and tap again for each lift.

Move in sixteenths of a pixel

birdy stores the vertical position in sixteenths of a pixel, and birdv stores the amount added to it each frame. Smaller screen Y values are higher up. A flap sets birdv to FLAPV = -80, an upward speed of 5 pixels per frame. During each PLAYING frame, gravity adds GRAV = 5 to that speed before the speed is added to birdy.

The starting position is STARTY16 = 196 × 16 = 3136. The first flap in READY sets the speed but leaves the position at 3136. If no further flap occurs, the next three frames are:

PLAYING framebirdv after gravitybirdy after movementDrawn Y (birdy >> 4)
1-753061191
2-702991186
3-652926182

The fraction stays in birdy even though drawbird uses asr.l #4 to get a whole pixel. That lets the speed change by 5 sixteenths each frame. Once gravity makes birdv positive, the bird descends; MAXFALL limits that downward speed.

Reuse three pipe records

pipes contains three records, each six bytes long. a0 points to one record at a time:

Offset from a0WordMeaning
0XLeft edge of the pipe.
2Gap centreVertical centre of its opening.
4Counted0 until its right edge passes the bird and scores a point.

newpipes starts their X positions at 620, 860 and 1100: SPACING = 240 pixels apart. Each PLAYING frame, movepipes subtracts SPEED = 4 from every X. Once a pipe has gone one width past the left edge, it jumps right by CYCLE = 720, which is three spacings. Its gap is chosen again and its counted word is cleared. It therefore joins the end of the same three-pipe course.

randgap changes a word called seed with multiplication and addition, then uses its high byte to choose the next gap centre. The offset is 0 through 179, so GAPMIN = 120 puts the centre at 120 through 299. The first flap gets a seed from task 8's elapsed time, making the course depend on when you start. GAPH = 140 makes each opening 140 pixels high, with HALFGAP = 70 above and below its centre. If you change the opening height, change its half-height too.

Check hits and show the result

hittest checks the ground first. It uses the bird's full height: when birdy / 16 + BIRDH reaches GROUNDY, the bird hits the ground. For a pipe, it uses a smaller rectangle. HITL and HITR inset the bird's horizontal span by HITIN = 6 pixels, and the tested top and bottom are also inset by 6. A pipe can cause a hit only while its X span overlaps this rectangle and the rectangle extends above or below the gap.

Task 92 mode 17 sets up a hidden drawing image. draw paints sky, pipes, ground, bird and text in that order, then task 94 shows the completed image. The drawing helpers supply the clouds, grass, wing and score; their details can wait until you want to change the artwork. Task 23 then waits at least DELAY = 3 hundredths of a second before the next frame. Drawing and instructions take time too, so this does not set an exact frame rate on every machine.

Try one small change: set DELAY to 6. Before running it, predict how the game will feel. Will the bird and pipes move a different number of pixels per frame?

Show what to expect

The minimum pause doubles, so the game will usually look slower in real time. GRAV, birdv and SPEED have not changed, so their movement per frame stays the same. The exact speed change in real time depends on the drawing and instruction time as well as the pause.