ZX81 Assembly Listing for birds.asm


ZX81 assembly listing for ***BIRDS**SLR/2026***

***BIRDS**SLR/2026*** (birds.asm)

Flocking birds using Boids simulation.


ASSEMBLY PROGRAM LISTING

; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;
; Birds
; Steven Reid (c) 2025,2026
; Simulates flocking behavior using Z80 assembly on the ZX81.
; 01/25/2025 - initial build
; 08/22/2025 - cleaned up routines, and more.
;
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;
; Header and startup
;

        ; start up stuff
        org 16514               ; stored in REM at top (ZX81)
        jr start                ; needed for z80asm

; title and copyright (will show when LISTed)
copy:
        db _as,_as,_as,_b_,_i_,_r_,_d_,_s_,_as,_as
        db _s_,_l_,_r_,_sl,_2_,_0_,_2_,_6_,_as,_as
        db _as,$76  ; ***birds**SLR/2025***


; starting routines (if any)
start:

        call slow               ; SLOW is required.
        call cls                ; clear screen / exapnd screen
        ld hl,(d_file)          ; save d_file (speeds up plot)
        ld (pf_screen_pos+1),hl

;
; end header and startup
;
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;
; Main program
;

        ; initalize program
initialize_program:
        call clear_screen               ; fast clear
        xor a
        ld (restart_requested),a        ; begin with no pending restart

        call init               ; Initialize variables
        call init_trees         ; vary tree positions for this run
        call draw_scenery       ; scenery is static, so draw it only once
        call render             ; draw the initial bird positions

main_loop:
        call calculate_flock    ; calculate the flock's center and direction
        call steer_birds        ; apply separation, cohesion, and alignment
        call erase_birds        ; erase birds immediately before moving them
        call update_birds       ; update bird positions and velocities
        call render             ; draw birds to the screen
        call delay              ; add a delay for smooth animation

        ; check_break sets restart_requested when a non-SPACE key is found.
        ; Perform the restart here, after the delay and its calls have
        ; returned normally, so no return addresses are left on the stack.
        ld a,(restart_requested)
        or a
        jp z,main_loop          ; no key, so draw the next frame normally

        call wait_key_release   ; prevent a held key from restarting again
        jp initialize_program  ; clear and build a completely new flock

;
; end main
;
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;
; Routines
;

; +++
; Initialize birds
init:
        ; initialize bird positions and velocities with random values
        ld b, max_birds
        ld hl, bird_data
init_loop:
        call random             ; generate random x position
        and screen_width-1      ; constrain to screen width
        ld (hl),a               ; store x position
        inc hl

        call random             ; generate random y position
        and 31                  ; start in the upper part of the sky
        add a,4                 ; keep away from the very top edge
        ld (hl),a               ; store y position
        inc hl

        call random             ; generate random x position
        and 3                   ; constrain to small range
        sub 2                   ; allow negative values (-2 to +1)
        ld (hl),a               ; store x velocity
        inc hl

        call random             ; generate random x position
        and 3                   ; constrain to small range
        sub 2                   ; allow negative values (-2 to +1)
        ld (hl),a               ; store y velocity
        inc hl

        djnz init_loop

        ret
; end init
; ---

; +++
; Initialize tree positions
;
; Each tree has a fixed base position that keeps the trees reasonably spaced
; across the screen. A random displacement from -3 through +4 pixels is added
; when the program starts. This produces a slightly different horizon for
; every run without allowing trees to collide or move beyond the screen.
init_trees:
        ld hl,tree_base_positions ; point to the evenly spaced base positions
        ld de,tree_positions      ; point to the positions used for drawing
        ld b,tree_count
init_trees_loop:
        call random             ; get a new random displacement
        and 7                   ; reduce to a value from 0 through 7
        sub 3                   ; convert to a value from -3 through +4
        add a,(hl)              ; add displacement to this base position
        ld (de),a               ; save the final tree position
        inc hl
        inc de
        djnz init_trees_loop
        ret
; end initialize tree positions
; ---

; +++
; Calculate flock values
;
; This routine calculates the average x and y locations of the flock. It
; also adds the signed x and y velocities so we know the general direction
; the flock is traveling. Only the sign of the velocity totals is needed.
;
; IX and IY are deliberately not used anywhere in this program. The ZX81
; display routine uses both index registers while running in SLOW mode.
; Changing either register, even briefly, can corrupt the display or system
; variables if a display interrupt happens at the wrong time.
calculate_flock:
        ld hl,bird_data         ; point to the first bird
        ld bc,0                 ; bc will hold the total x positions
        ld de,0                 ; de will hold the total y positions
        ld a,max_birds
        ld (flock_count),a      ; use memory so bc remains available for sum

calculate_position_loop:
        ld a,(hl)               ; get this bird's x position
        add a,c                 ; add it to the low byte of the x total
        ld c,a
        jr nc,calculate_x_ok
        inc b                   ; carry into the high byte of the x total
calculate_x_ok:
        inc hl                  ; move to the y position
        ld a,(hl)
        add a,e                 ; add it to the low byte of the y total
        ld e,a
        jr nc,calculate_y_ok
        inc d                   ; carry into the high byte of the y total
calculate_y_ok:
        inc hl                  ; move past y
        inc hl                  ; move past x velocity
        inc hl                  ; move past y velocity to the next bird

        ld a,(flock_count)
        dec a
        ld (flock_count),a
        jr nz,calculate_position_loop

        ; Divide the x total by max_birds to get the average x position.
        ; Using the same assembly constant here means the bird count only
        ; needs to be changed in one place near the bottom of the program.
        push de                 ; save the y total while dividing x
        ld h,b
        ld l,c
        call divide_by_bird_count
        ld a,l
        ld (flock_x),a

        ; Divide the y total by max_birds to get the average y position.
        pop hl
        call divide_by_bird_count
        ld a,l
        ld (flock_y),a

        ; Add the signed velocity bytes. Up to 63 birds with velocities from
        ; -2 to +2 fit safely in a signed eight-bit total.
        ld hl,bird_data+2       ; point to the first x velocity
        ld d,0                  ; d will hold the signed x velocity total
        ld e,0                  ; e will hold the signed y velocity total
        ld b,max_birds
calculate_velocity_loop:
        ld a,(hl)               ; get x velocity
        add a,d
        ld d,a
        inc hl                  ; point to y velocity
        ld a,(hl)
        add a,e
        ld e,a
        inc hl                  ; advance from y velocity to next bird x
        inc hl
        inc hl                  ; point to the next bird's x velocity
        djnz calculate_velocity_loop

        ld a,d
        call signed_direction   ; convert total to -1, 0, or +1
        ld (flock_vx),a
        ld a,e
        call signed_direction
        ld (flock_vy),a
        ret
; end calculate flock values
; ---

; +++
; Divide HL by the configured bird count
;
; The position totals are small, so repeated subtraction is compact and fast
; enough for this program. max_birds is an assembly-time tuning constant, so
; changing the number of birds automatically changes this divisor as well.
; The integer quotient is returned in HL.
divide_by_bird_count:
        ld de,0                 ; de will count the quotient
        ld bc,max_birds         ; use the configured number of birds
divide_by_bird_count_loop:
        or a                    ; clear carry before subtraction
        sbc hl,bc
        jr c,divide_by_bird_count_done
        inc de
        jr divide_by_bird_count_loop
divide_by_bird_count_done:
        ex de,hl                ; return the quotient in hl
        ret
; end divide by configured bird count
; ---

; +++
; Convert signed value to direction
;
; in:  a = signed value
; out: a = $ff (-1), 0, or 1
signed_direction:
        or a
        ret z                   ; zero total means no preferred direction
        bit 7,a
        jr z,signed_positive
        ld a,$ff                ; return -1
        ret
signed_positive:
        ld a,1                  ; return +1
        ret
; end signed direction
; ---

; +++
; Steer birds
;
; The ZX81 has very little processing time available while generating the
; display. Each bird therefore compares itself with the next bird in a
; circular chain rather than scanning every possible pair. This still gives
; a local separation response without requiring a costly nested loop.
;
; If the neighboring bird is within three pixels in both directions,
; separation is applied. Otherwise the bird gently moves toward the flock
; center (cohesion), and a stopped bird adopts the flock direction
; (alignment).
steer_birds:
        ld hl,bird_data         ; point to the current bird
        ld b,max_birds
steer_birds_loop:
        push bc                 ; save the outer loop counter
        ld (current_bird),hl    ; save the current record address

        ; The next record is the neighbor. The final bird wraps around and
        ; uses the first bird as its neighbor.
        ld a,b
        cp 1
        jr z,steer_wrap_neighbor
        ld de,4
        add hl,de
        jr steer_neighbor_ready
steer_wrap_neighbor:
        ld hl,bird_data
steer_neighbor_ready:
        ld a,(hl)
        ld (neighbor_x),a
        inc hl
        ld a,(hl)
        ld (neighbor_y),a

        ld hl,(current_bird)    ; restore the current bird pointer
        ld a,(hl)
        ld (current_x),a
        inc hl
        ld a,(hl)
        ld (current_y),a

        call calculate_separation
        ld a,(near_neighbor)
        or a
        jr z,steer_cohesion

        ; Close birds steer away from one another. Separation takes priority
        ; over the other two rules for this frame.
        inc hl                  ; point to x velocity
        ld a,(separation_x)
        add a,(hl)
        call clamp_velocity
        ld (hl),a
        inc hl                  ; point to y velocity
        ld a,(separation_y)
        add a,(hl)
        call clamp_velocity
        ld (hl),a
        jr steer_birds_next

steer_cohesion:
        ; HL currently points to the y position. Move to x velocity.
        inc hl
        ld a,(hl)
        ld (working_velocity),a
        ld a,(flock_x)
        ld c,a
        ld a,(current_x)
        call steer_toward_value
        ld a,(working_velocity)
        or a
        jr nz,steer_x_ready
        ld a,(flock_vx)         ; alignment restarts a stopped bird
        ld (working_velocity),a
steer_x_ready:
        ld a,(working_velocity)
        call clamp_velocity
        ld (hl),a

        ; Repeat cohesion and alignment for the y velocity.
        inc hl
        ld a,(hl)
        ld (working_velocity),a
        ld a,(flock_y)
        ld c,a
        ld a,(current_y)
        call steer_toward_value
        ld a,(working_velocity)
        or a
        jr nz,steer_y_ready
        ld a,(flock_vy)
        ld (working_velocity),a
steer_y_ready:
        ld a,(working_velocity)
        call clamp_velocity
        ld (hl),a

steer_birds_next:
        ld hl,(current_bird)
        ld de,4
        add hl,de               ; advance to the next bird record
        pop bc
        dec b                   ; DJNZ cannot reach the large commented loop
        jp nz,steer_birds_loop
        ret
; end steer birds
; ---

; +++
; Calculate separation
;
; Sets near_neighbor when the current and neighboring birds are within the
; separation radius on both axes. The separation values point away from the
; neighbor and are added to the current velocity.
calculate_separation:
        xor a
        ld (near_neighbor),a

        ld a,(current_x)
        ld c,a
        ld a,(neighbor_x)
        ld b,a
        ld a,c
        sub b                    ; signed distance = current - neighbor
        call separation_direction
        ret nc                   ; too far apart in x
        ld (separation_x),a

        ld a,(current_y)
        ld c,a
        ld a,(neighbor_y)
        ld b,a
        ld a,c
        sub b
        call separation_direction
        ret nc                   ; too far apart in y
        ld (separation_y),a

        ld a,1
        ld (near_neighbor),a
        ret
; end calculate separation
; ---

; +++
; Convert separation distance to direction
;
; in:  a = signed distance
; out: a = $ff, 0, or 1
;      carry set if within separation radius
separation_direction:
        or a
        jr z,separation_zero
        bit 7,a
        jr z,separation_positive
        neg                     ; get the absolute negative distance
        cp separation_radius+1
        ret nc                  ; carry clear means too far away
        ld a,$ff                ; current bird is left/above neighbor
        scf
        ret
separation_positive:
        cp separation_radius+1
        ret nc
        ld a,1                  ; current bird is right/below neighbor
        scf
        ret
separation_zero:
        xor a                   ; no steering on this particular axis
        scf
        ret
; end separation direction
; ---

; +++
; Steer one velocity toward a position
;
; in:  a = current position
;      c = flock center position
;      working_velocity = current signed velocity
;
; A bird only turns toward the center when it is at least
; cohesion_radius pixels away. This prevents constant jitter near the center.
steer_toward_value:
        ld b,a                  ; save current position
        ld a,c                  ; center - current gives desired direction
        sub b
        ret z
        bit 7,a
        jr nz,steer_toward_lower
        cp cohesion_radius
        ret c
        ld a,(working_velocity)
        inc a
        ld (working_velocity),a
        ret
steer_toward_lower:
        cp 257-cohesion_radius
        ret nc
        ld a,(working_velocity)
        dec a
        ld (working_velocity),a
        ret
; end steer toward value
; ---

; +++
; Clamp velocity
;
; Keeps signed velocity in the range -2 through +2.
clamp_velocity:
        bit 7,a
        jr nz,clamp_negative
        cp max_velocity+1
        ret c
        ld a,max_velocity
        ret
clamp_negative:
        cp 256-max_velocity
        ret nc
        ld a,256-max_velocity
        ret
; end clamp velocity
; ---

; +++
; Update bird locations
update_birds:
        ld b, max_birds         ; loop counter
        ld hl, bird_data        ; start of bird data
update_loop:
        push bc

        ; load x position and velocity into registers
        push hl
        ld d,(hl)               ; load x position into d
        inc hl
        ld e,(hl)               ; load y position into e
        inc hl
        ld b,(hl)               ; load x velocity into b
        inc hl
        ld c,(hl)               ; load y velocity into c
        pop hl

        ; Update x position. Birds bounce at the left and right edges rather
        ; than wrapping to the opposite side of the screen.
        ld a,d                  ; a = x position
        add a,b                 ; a = x position + x velocity
        bit 7,a
        jr nz,bounce_x_left     ; negative position means left edge crossed
check_x_width:
        cp screen_width
        jr c,store_x            ; position is still within the screen

bounce_x_right:
        ld d,screen_width-1     ; constrain bird to rightmost pixel
        ld a,b
        neg                     ; reverse horizontal velocity
        ld b,a
        jr update_y_position

bounce_x_left:
        ld d,0                  ; constrain bird to leftmost pixel
        ld a,b
        neg                     ; reverse horizontal velocity
        ld b,a
        jr update_y_position

store_x:
        ld d,a                  ; store updated x position into d

update_y_position:
        ; Update y position. The lower boundary is the bottom of the sky,
        ; leaving rows 44 through 47 permanently reserved for scenery.
        ld a,e                  ; a = y position
        add a,c                 ; a = y position + y velocity
        bit 7,a
        jr nz,bounce_y_top      ; negative position means top edge crossed
check_y_height:
        cp screen_height
        jr c,store_y            ; position is still within the sky

bounce_y_bottom:
        ld e,screen_height-1    ; constrain bird to lowest sky pixel
        ld a,c
        neg                     ; reverse vertical velocity
        ld c,a
        jr store_bird_data

bounce_y_top:
        ld e,0                  ; constrain bird to topmost pixel
        ld a,c
        neg                     ; reverse vertical velocity
        ld c,a
        jr store_bird_data

store_y:
        ld e,a                  ; store updated y position into e

        ; store updated values back to bird_data
store_bird_data:
        ld a,d                  ; store x position
        ld (hl),a
        inc hl
        ld a,e                  ; store y position
        ld (hl),a
        inc hl
        ld a,b                  ; store x velocity
        ld (hl),a
        inc hl
        ld a,c                  ; store y velocity
        ld (hl),a
        inc hl

        pop bc
        djnz update_loop       ; repeat for next bird
        ret
; end update birds
; ---

; +++
; Render birds to screen
;
; The screen is not cleared here. All old bird pixels are removed before the
; flock is updated, and all new bird pixels are drawn afterward. This avoids
; the visible flash caused by clearing and redrawing the complete display.
render:
        ld b,max_birds
        ld hl,bird_data
render_loop:
        push bc

        ld a,(hl)               ; get x position
        ld c,a
        inc hl
        ld a,(hl)               ; get y position
        ld b,a
        inc hl
        push hl

        call plot_pixel         ; plot bird

        pop hl
        inc hl                  ; push past velocity
        inc hl
        pop bc

        djnz render_loop

        ret
; end render
; ---

; +++
; Erase birds from screen
;
; Every bird is erased before any position is changed. All birds are then
; redrawn after the update. Doing the erase as a separate pass is important:
; if two birds share a pixel, a later erase cannot accidentally remove a bird
; that has already been drawn in its new position.
;
; The birds are constrained to y positions 0 through 43, so this routine can
; never erase any part of the horizon or trees on rows 45 through 47.
erase_birds:
        ld b,max_birds
        ld hl,bird_data
erase_birds_loop:
        push bc

        ld a,(hl)               ; get x position
        ld c,a
        inc hl
        ld a,(hl)               ; get y position
        ld b,a
        inc hl
        push hl

        call clear_pixel        ; remove bird from its old position

        pop hl
        inc hl                  ; move past x velocity
        inc hl                  ; move past y velocity
        pop bc
        djnz erase_birds_loop
        ret
; end erase birds
; ---

; +++
; Draw scenery
;
; Draws a thin line across the very bottom of the 64 by 48 pixel display.
; Five tiny trees sit on the line. Each tree uses one trunk pixel and three
; canopy pixels, so the scenery remains simple and does not distract from
; the birds.
draw_scenery:
        ld b,ground_y           ; y position of the bottom horizon line
        ld c,0                  ; begin at the left edge
draw_horizon_loop:
        push bc                 ; plot_pixel changes b and c
        call plot_pixel
        pop bc
        inc c
        ld a,c
        cp screen_width
        jr c,draw_horizon_loop

        ld hl,tree_positions    ; list of tree center x positions
        ld a,tree_count
draw_tree_loop:
        push af                 ; save remaining tree count
        ld a,(hl)
        ld (tree_x),a
        inc hl

        ; Draw the one-pixel trunk directly above the ground.
        ld c,a
        ld b,ground_y-1
        push hl
        call plot_pixel
        pop hl

        ; Draw a three-pixel canopy one row above the trunk.
        ld a,(tree_x)
        dec a
        ld c,a
        ld b,ground_y-2
        push hl
        call plot_pixel
        pop hl

        ld a,(tree_x)
        ld c,a
        ld b,ground_y-2
        push hl
        call plot_pixel
        pop hl

        ld a,(tree_x)
        inc a
        ld c,a
        ld b,ground_y-2
        push hl
        call plot_pixel
        pop hl

        pop af
        dec a
        jr nz,draw_tree_loop
        ret
; end draw scenery
; ---

; +++
; Clear screen - my fast routine, assumes expanded display
clear_screen:
        ld hl,(d_file)
        inc hl
        xor a
        ld c,24         ; rows
fastcls_y_loop:
        ld b,32         ; columns
fastcls_x_loop:
        ld (hl),a       ; clear character
        inc hl
        djnz fastcls_x_loop

        inc hl          ; move past return

        dec c
        jr nz,fastcls_y_loop

        ret
; end clear screen
; ---


; +++
; Xperiment's plot routine (with additional tweaks)
; b = y, c = x
;
; To use, make sure to put these lines at the top:
;        ld hl,(d_file)          ; save d_file (speeds up)
;        ld (pf_screen_pos+1),hl

plot_pixel:
        ; check bounds (comment out if confident this isn't an issue)
        ld a,c          ; ld a with x
        bit 7,a
        ret nz          ; out of bounds
        sub 64
        ret p           ; out of bounds

        ld a,b          ; ld a with y
        bit 7,a
        ret nz          ; out of bounds
        sub 48
        ret p           ; out of bounds
        ; end check bounds

        ld d,1
        sra b
        jp nc, plotPass2Fast
        ld d,4
plotPass2Fast:
        sra c
        jp nc, plotPass1Fast
        sla d

plotPass1Fast:
        push de

        ld l,b
        ld h,0
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        ex de,hl
        ld a,b
        add a,e
        ld e,a
pf_screen_pos:
        ld hl,$00
        inc hl
        add hl,de
        ld b,0
        add hl,bc

        pop de
        ld b,(hl)
        bit 7,b
        ld a,b
        jp z,skip_charToValFast
        res 7,b
        ld a,15
        sub b

skip_charToValFast:
        or d
        ld d,a
        bit 3,a
        jp z,skip_valToCharFast
        ld a,15
        sub d
        set 7,a

skip_valToCharFast:
        ld (hl),a
        ret
; end of plot_pixel
; ---

; +++
; Xperiment's plot routine adapted to clear one pixel
; b = y, c = x
;
; This follows the same address and ZX81 graphics-character conversion used
; by plot_pixel. The only difference is that the selected pixel bit is
; cleared instead of set. Keeping the two routines parallel makes it easier
; to compare and maintain them.
clear_pixel:
        ; check bounds (same checks used by plot_pixel)
        ld a,c                  ; ld a with x
        bit 7,a
        ret nz                  ; out of bounds
        sub 64
        ret p                   ; out of bounds

        ld a,b                  ; ld a with y
        bit 7,a
        ret nz                  ; out of bounds
        sub 48
        ret p                   ; out of bounds
        ; end check bounds

        ; Build the bit mask for this pixel inside its 2 by 2 graphics
        ; character. The mask is returned in d.
        ld d,1
        sra b
        jp nc,clearPass2Fast
        ld d,4
clearPass2Fast:
        sra c
        jp nc,clearPass1Fast
        sla d

clearPass1Fast:
        push de                 ; save the pixel mask

        ; Calculate the display-file address for y * 32 + x. Each display
        ; row is followed by a $76 newline byte, so b is added once more.
        ld l,b
        ld h,0
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        ex de,hl
        ld a,b
        add a,e
        ld e,a
        ld hl,(d_file)
        inc hl
        add hl,de
        ld b,0
        add hl,bc

        pop de                  ; restore pixel mask in d

        ; Convert the ZX81 graphics character into its four-bit pixel value.
        ld b,(hl)
        bit 7,b
        ld a,b
        jp z,clear_charToValFast
        res 7,b
        ld a,15
        sub b

clear_charToValFast:
        ; Clear the selected bit while leaving the other three pixels alone.
        ld e,a                  ; save the current four-bit pixel value
        ld a,d                  ; get the selected pixel mask
        cpl                     ; invert mask so AND clears only this pixel
        and e
        ld d,a                  ; d now holds the updated pixel value

        ; Convert the four-bit pixel value back to a ZX81 graphics character.
        bit 3,a
        jp z,clear_valToCharFast
        ld a,15
        sub d
        set 7,a

clear_valToCharFast:
        ld (hl),a
        ret
; end of clear_pixel
; ---

; +++
; Random routine
;
;       Returns a pseudo-random number between 0 and 255.
;
; The original routine used the refresh register to select bytes from ROM.
; That is compact, but repeated calls during initialization can select related
; locations and make the bird positions look patterned. This routine keeps an
; eight-bit state and applies several XOR and shift operations to scramble it.
; The ZX81 frame counter and refresh register are mixed into the state so a
; different load or start time produces a different initial flock.
;
; A zero state is explicitly changed to one because an all-zero xorshift state
; would otherwise remain zero forever.
;
; in:           N/A
; out:          a
; preserves:    bc
; destroys:     af
random:
        push bc

random_seed:
        ld a,$a5                ; begin with the previous random state
        ld b,a
        ld a,(frames)           ; mix in the low byte of the frame counter
        xor b
        ld b,a
        ld a,r                  ; mix in the Z80 refresh register as well
        xor b
        or a
        jr nz,random_seed_ok
        inc a                   ; never allow an all-zero state
random_seed_ok:
        ld b,a                  ; x ^= x rotated right three places
        rrca
        rrca
        rrca
        xor b
        ld b,a                  ; x ^= x shifted left one place
        sla a
        xor b
        ld b,a                  ; x ^= x shifted right two places
        srl a
        srl a
        xor b
        ld (random_seed+1),a         ; save state for the next call

        pop bc
        ret                     ; return random value in a
; end random
;
; ---

; +++
; Break
;
; Preserves state, exits if SPACE is pushed, and requests a restart if any
; other key is pushed. The restart itself is performed by the main loop after
; this routine and the delay routine have returned normally.
check_break:
        exx                     ; save register states

        ; did the player press break key (space)?
        call $0f46              ; was break pressed? (break-1 ROM routine)
        jr nc,break             ; no, exit as normal

        ; SPACE was not pressed. LAST_K contains the most recently scanned
        ; keyboard row and bit information. A value of $ff in c means that
        ; the ROM keyboard scan has not found any key.
        ld bc,(last_k)
        inc c                   ; $ff becomes zero when no key is pressed
        jr z,check_key_done

        ld a,1
        ld (restart_requested),a ; tell the main loop to restart the program

check_key_done:
        exx                     ; restore registers
        ret                     ; and return

        ; yes, exit the program as normal
break:
        rst $0008               ; call ERROR-1 reset
        db $ff                  ; with error code 0 (normal exit)
; end break
; ---

; +++
; Wait for key release
;
; A restart should happen once for each key press. Waiting until LAST_K shows
; no key prevents a held key from immediately restarting the newly created
; flock over and over. The ZX81 ROM continues updating LAST_K while the
; machine is operating in SLOW mode.
wait_key_release:
        ld bc,(last_k)
        inc c                   ; $ff means all keys have been released
        ret z
        jr wait_key_release
; end wait for key release
; ---

; +++
; Delay
;
; set bc to speed
; uses check_break to exit
delay_count: dw $0000
delay:
        ld hl,frame_delay       ; time to delay
delay_loop:
        ld (delay_count),hl     ; save delay

        call check_break

        ; check if done
        ld hl,(delay_count)     ; grab what to test
        dec hl                  ; subtract 1
        ld a,h                  ; check if done
        or l
        jr nz,delay_loop        ; not zero, keep going!

        ret                     ; pause is done!
; end delay
; ---

;
; end routines
;
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;
; Data
;

; constants
frame_delay:            equ 100 ; frame delay time
max_birds:              equ 20  ; number of birds in the simulation (main tuning value)
screen_width:           equ 64  ; zx81 screen width in characters
screen_height:          equ 44  ; sky height (bottom rows are for scenery)
ground_y:               equ 47  ; very bottom pixel row
max_velocity:           equ 3   ; maximum signed velocity for birds
cohesion_radius:        equ 12  ; distance before steering toward flock center
separation_radius:      equ 3   ; distance considered too close to another bird
tree_count:             equ 5   ; number of tiny trees along the horizon

; memory layout
bird_data:      defs max_birds*4
;       birds are layed out like this:
;       x_coord, y_coord, x_velocity, y_velocity

; calculated flock values
flock_x:        db 0    ; average x position of all birds
flock_y:        db 0    ; average y position of all birds
flock_vx:       db 0    ; general horizontal flock direction (-1, 0, +1)
flock_vy:       db 0    ; general vertical flock direction (-1, 0, +1)
flock_count:    db 0    ; temporary loop count used while totaling positions

; temporary steering values
current_bird:   dw 0    ; address of the bird currently being steered
current_x:      db 0    ; current bird x position
current_y:      db 0    ; current bird y position
neighbor_x:     db 0    ; neighboring bird x position
neighbor_y:     db 0    ; neighboring bird y position
near_neighbor:  db 0    ; nonzero when the neighbor is within separation range
separation_x:   db 0    ; horizontal direction away from neighbor
separation_y:   db 0    ; vertical direction away from neighbor
working_velocity: db 0  ; velocity being adjusted by a steering rule

; x positions of the tiny trees
tree_x:                 db 0
tree_base_positions:    db 6,18,31,46,57
tree_positions:         defs tree_count

; keyboard action flag
restart_requested:      db 0    ; nonzero requests a new flock and scenery

;
; end data
;
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
;
; Defines
;

; ZX81 system vars
d_file:         equ $400c
df_cc:          equ 16398
last_k:         equ 16421
margin:         equ 16424
s_posn:         equ 16441
frames:         equ 16436

; ZX81 ROM functions
kscan:          equ $02bb
findchar:       equ $07bd
stop:           equ $0cdc
slow:           equ $0f2b
fast:           equ $02e7
save:           equ $02f9
printat:        equ $08f5
pause:          equ $0f35
cls:            equ $0a2a

; ZX81 Characters (not ASCII)
_sp:            equ $00
_qu:            equ $0b
_lb:            equ $0c
_dl:            equ $0d
_cl:            equ $0e
_lp:            equ $10
_rp:            equ $11
_gt:            equ $12
_lt:            equ $13
_eq:            equ $14
_pl:            equ $15
_mi:            equ $16
_as:            equ $17
_sl:            equ $18
_sc:            equ $19
_cm:            equ $1a
_pr:            equ $1b
_0_:            equ $1c
_1_:            equ $1d
_2_:            equ $1e
_3_:            equ $1f
_4_:            equ $20
_5_:            equ $21
_6_:            equ $22
_7_:            equ $23
_8_:            equ $24
_9_:            equ $25
_a_:            equ $26
_b_:            equ $27
_c_:            equ $28
_d_:            equ $29
_e_:            equ $2a
_f_:            equ $2b
_g_:            equ $2c
_h_:            equ $2d
_i_:            equ $2e
_j_:            equ $2f
_k_:            equ $30
_l_:            equ $31
_m_:            equ $32
_n_:            equ $33
_o_:            equ $34
_p_:            equ $35
_q_:            equ $36
_r_:            equ $37
_s_:            equ $38
_t_:            equ $39
_u_:            equ $3a
_v_:            equ $3b
_w_:            equ $3c
_x_:            equ $3d
_y_:            equ $3e
_z_:            equ $3f

;
; end defines
;
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *