Flocking birds using Boids simulation.
; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ; ; 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 ; ; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *