Skip to content

A Soft Baa

Paula speaks: one looping square wave, and three farmyard sounds made of nothing but pitch, volume and time — a baa on the hop, a squelch for a loss, a rising bleat at the pen.

  • Commodore Amiga
  • Assembly

The farm has a voice now.

Hold the stick and she trots upfield to a staccato baa-baa-baa. Lose her and the lane answers with a low squelch. Get her home and the pen greets her with a rising bleat. Three sounds, and they’re all the same eight bytes — what differs is pitch, volume and time, which is most of what sound design is.

Listen to a real run — the trot, and (this being an honest farmyard) one squelch along the way:

The Display

Flock Unit 11

One home, one lost, the third sheep up — the run you just heard.

Paula Plays Loops

Paula’s model is unlike the beepers and tone chips of the 8-bit world: each of her four channels loops a sample from Chip RAM on its own DMA. Point the channel at some bytes, tell her how many, how fast (the period — bigger is lower), and how loud; switch the channel’s DMA on; she plays it round and round until you stop her. The CPU does not write each sample, but setting up and updating the sound still takes CPU time, and audio DMA uses Chip RAM bus time.

Our “sample” is the humblest one possible — one cycle of a square wave, eight signed bytes:

Assembly
squarewave: dc.b    64,64,64,64,-64,-64,-64,-64

Loop that fast enough and it’s a tone. Every pitch in the game is this same wave at a different period. (Real sampled sound — a recorded baa — is the same machinery with more bytes; a later game could climb that rung, and now you know exactly what it’ll cost.)

Two Notes and a Clock

01-playsound.asm
playsound:
            lea     squarewave,a0
            move.l  a0,AUD0LC(a5)       ; The wave to loop
            move.w  #4,AUD0LEN(a5)      ; Four words = eight samples
            move.w  d0,AUD0PER(a5)      ; Pitch now...
            move.w  d1,sndper2          ; ...pitch later
            move.w  d3,AUD0VOL(a5)
            move.w  d2,sndtimer
            lsr.w   #1,d2
            move.w  d2,sndhalf          ; Where the wobble happens
            move.w  #$8001,DMACON(a5)   ; SET + AUD0EN: sing
            rts

;──────────────────────────────────────────────────────────────
; soundtick — once per frame: wobble at halfway, stop on time
;──────────────────────────────────────────────────────────────
soundtick:
            tst.w   sndtimer
            beq.s   .quiet
            subq.w  #1,sndtimer
            bne.s   .wobble
            move.w  #$0001,DMACON(a5)   ; CLR + AUD0EN: hush
            move.w  #0,AUD0VOL(a5)
            rts
.wobble:
            move.w  sndtimer,d0
            cmp.w   sndhalf,d0          ; Halfway through?
            bne.s   .quiet
            move.w  sndper2,AUD0PER(a5) ; The second note
.quiet:
            rts

playsound is the entire voice: wave, length, pitch, volume, DMA on — and three words of state for soundtick to act on, once per frame. At halfway, the period slides to the second note; at zero, DMA off and volume down. That mid-flight slide is the whole character of each sound: the baa droops (1800 → 2100), the squelch sags (2800 → 3600), the bleat rises (1700 → 1250). Pitch movement is emotion — down is deflating, up is delight — and it costs one register write.

Our current policy is new sounds replace the channel’s previous sound. A squelch can replace a baa; there is no queue or mixing. The latest call wins because playsound writes new hardware settings and replaces sndtimer, sndhalf and sndper2. This is a design choice, not a rule that the newest event is always the most important.

Who owns the sound now?

We can trace a conflict without changing the game. Imagine these two calls occur before the next soundtick: first the hop’s baa, then the pen’s bleat. The sample bytes remain the same; the settings and countdowns change.

After this call Starting period Later period Timer Halfway count Volume
Baa 1800 2100 5 2 40
Bleat 1700 1250 18 9 50

lsr.w #1,d2 makes the halfway count: half of five is rounded down to two. At the next soundtick, the timer falls from 18 to 17. There is no saved baa waiting to resume. Reverse the two calls on paper: the five-frame baa now wins. That might hide the more useful message that a sheep reached the pen.

For the main game, keep the current behaviour. The optional comparison below tries a different policy in a separate copy. When we add a title sequence in unit 17, we will need to coordinate successive notes too. Another hardware channel would need separate countdown state and an ownership rule of its own.

Follow a hop beyond the input routine

Use the complete source below. Before tracing, assume won=0, gameover=0, squashtimer=0, cooldown=0, sheepy=200 and curframe=0. The stick is held up; this move stays in the starting field and does not collide or reach a pen. Predict the sheep’s position, picture index and sound timer after one whole main-loop pass. Does returning from steer mean the sound has finished?

Hint 1: follow the callers, not only the sound routine

The main loop calls steer, drivelanes, checksquash, soundtick, updsprite and showframe in that order. Find which work happens after playsound returns.

Hint 2: separate state from presentation

STEP is 8. The accepted hop toggles curframe and sets cooldown; the baa initialises sndtimer to 5. Later in the same pass, soundtick visits that timer.

Compare the trace

steer reads and decodes the joystick, changes sheepy to 192, sets cooldown to 6 and toggles curframe to 1. It requests the baa and returns while hardware playback continues. Traffic and collision checks follow; under our stated conditions there is no replacement cue. soundtick reduces the new timer from 5 to 4. The later sprite routines prepare position and artwork for the display hardware. A CPU write is not proof that the player has already seen the new picture.

Now start a separate paper trace with cooldown=1. steer reduces it to zero and returns before reading the joystick. That pass accepts no new hop or baa request; the rest of the main loop still runs, including the countdown for any sound already playing. Holding up can lead to a hop on the next ready pass. A brief press entirely between eligible readings may not be observed.

Explain which state belongs to movement, animation and sound, and point to the instructions connecting them. Then consider an unfamiliar fault: the sheep reaches its new position and changes feet, but a sound already playing cuts off there. Which request history would distinguish normal channel replacement from an incorrect sound timer? Use the earlier ownership trace before changing sample data. A correct position does not diagnose an audio fault.

The essential exercise is the trace and one evidence-based explanation. Composing a new sound is optional. For a transfer challenge, explain how the same idea—use an accepted action for both rules and feedback—appears in Touchdown’s burn flag. The input and sound mechanisms differ; we are comparing the responsibility.

Optional experiment: let the important cue finish

Use the sound-ownership companion and build instructions. It produces two native Amiga disks from this unit’s source: latest wins and priority. Gameplay is frozen for the experiment. We watch and hear three pairs of requests rather than trying to arrange an accidental collision.

Give the hop priority 1 and the pen priority 2. An idle channel accepts either. A busy channel accepts only a strictly higher priority; an equal or lower request is discarded. Nothing waits to resume. Before running, predict which cue remains after hop→pen, pen→hop and pen→pen.

The bottom-right display now reads AA RR: two digits for accepted requests, two for rejected requests in the current pair. It is no longer the score. The pairs occur 60 updates apart; cold-boot each disk to repeat the trial. These visible counts let us investigate even with sound muted.

requestsound.asm
; Main-loop only: no interrupt calls this routine or changes its state.
; d0/d1: periods; d2: positive duration; d3: volume; d4: priority 1 or 2.
; Clobbers: a0, d2, condition codes (as playsound does). No queued requests.
requestsound:
            ifne    USE_PRIORITY
            tst.w   sndtimer
            beq.s   .accept             ; Idle: no owner to displace
            cmp.w   sndpriority,d4
            bhi.s   .accept             ; Higher unsigned priority wins
            addq.w  #1,rejected         ; Equal/lower: leave ALL sound state alone
            rts
            endc
.accept:
            move.w  d4,sndpriority
            addq.w  #1,accepted
            bra     playsound           ; Existing playback setup, unchanged

d4 supplies the request’s priority. The other parameters still describe the same sound. On rejection we leave the period, volume and countdown untouched. On acceptance we record the owner and use the original playsound. When soundtick reaches zero, the companion clears sndpriority alongside the existing sound stop. These routines run sequentially in the main loop; we have not built a safe handoff from an interrupt handler.

Hint: separate accepting a request from hearing a whole cue

Two requests can both be accepted in one update, yet the second replaces the first before we hear it clearly. Follow the calls in order, checking whether the timer is zero and whether the new priority is strictly greater.

Compare the two policies

Latest-wins shows 0200 for all three pairs. Pen, hop and pen remain, respectively. Priority shows 0200, 0101, 0101, keeping the pen cue in every pair. The later hop no longer hides arrival feedback.

For pen→pen, equal priority keeps the first request. Both calls occur in one update, so the diagnostic count establishes the rejection; this pair does not demonstrate an audible gap or retrigger. Rejected cues are lost, not queued. A queue might instead play a hop after it has stopped being relevant.

The fixture calls soundtick before making requests and records their full timers. The ordinary game calls it afterwards. Keep that ordering in mind when we trace a whole hop below.

Explain why the second request in pen→hop changes the sound under one policy but changes only a diagnostic count under the other. Then choose a tie policy for repeated hops and explain its cost: does repeating a cue communicate useful action, or obscure the sound already playing? Either answer needs a game reason. The normal game remains our recovery copy; do not continue later units from the frozen experiment.

Hear a request arrive after playback has begun

The companion’s --spaced option uses the same sounds and replacement rules, but leaves six updates between competing requests. Predict the difference before playing these recordings. They are four-second excerpts from native A500 PAL emulation, with the same gain and a short lead-in; no sound has been synthesised or rearranged for the comparison.

Latest wins

Higher priority wins; equal priority keeps the current cue

The sequence is: pen with two later hops; a hop after the channel is idle; three spaced pen requests; a hop at the final possible pen-expiry boundary; then one original pen cue on its own. The last cue checks restoration of the original settings. It is a new request, not a stolen sound resuming.

We can investigate with the recordings muted too. The companion records each request’s timer, priority and accepted/rejected counts. Use these checkpoints:

Update Request or observation Predict what changes
1, 7, 13 Pen, hop, hop Does the pen finish, or do the hops replace it?
31 Hop after either policy is idle Can a low-priority cue play again?
61, 67, 73 Pen, pen, pen Does the timer restart at 18 or keep falling?
91 Hop after this update’s soundtick Has the latest possible pen ended?
97 No request Are timer and ownership both zero?
121, 139 Original pen, then its expiry Do the original settings and release still work?
Hint: inspect the timer before deciding that a cue is stuck

With priority, the pen’s timer is 18, then 12, then 6 at the three competing request points. Rejection does not refresh it. Under latest-wins, each accepted pen resets it to 18. At update 91, the last pen requested at 73 reaches zero before the hop asks for the channel.

Compare interruption, retriggering and release

Latest-wins replaces the first pen with a hop. In the repeated-pen group, each request restarts the software timer and pitch settings, extending the cue and returning it to its starting pitch. This does not promise a phase-aligned restart of Paula’s sample DMA; the companion retains the original hardware setup.

Priority keeps the first pen through the hops and rejects both equal-priority pen repeats. In each group the accepted/rejected totals end at 0102, rather than latest-wins’ 0300. The priority cue still expires: at updates 31 and 91, the hop is accepted under both policies. At 97, both timer and priority are zero. The final pen starts with its original 18-update duration and ends at 139.

Listen again: which version makes arrival clearer, and which reports more of the later activity? A cleaner arrival message costs us those hop sounds. If that lost information matters, we need a different rule or another voice—not merely higher priorities for everything. Audio alone does not tell us which state was wrong; use the records and whole-hop trace.

After experimenting, return to the original unit-11 game source; the companion has not changed it. In unit 17’s title music, tunetick requests successive notes, and startgame stops the title audio when play begins. That version separates musical and gameplay use by game state. It does not yet mix the title tune with gameplay effects. Explain which extra ownership decision would be needed if both could request this channel together.

The sound requests sit at the event sites: the hop in steer, loss in checksquash and arrival in trypen. Keeping each request beside the accepted event helps us trace why it played.

Experiment: Tune the Farmyard

  • BAA_PER1 equ 900 — a lamb. equ 3000 — a sheep the size of a barn. Pitch is character.
  • Swap each sound’s two periods. The baa now rises, the bleat droops. Listen to how wrong the same events feel — then put joy back where it belongs.
  • BAA_FRAMES equ 12 and hold up: the baas merge into a drone (each hop re-steals the channel before the last baa ends). The first cut of this unit shipped exactly that by accident — five frames is what made the trot staccato.
  • A gentler wave: dc.b 0,45,64,45,0,-45,-64,-45 — eight points of a sine-ish curve. Same notes, rounder voice. The waveform is timbre; the period is pitch; you now control both.

The Complete Code

flock.asm
;──────────────────────────────────────────────────────────────
; FLOCK - A sheep-crossing arcade game for the Commodore Amiga
; Unit 11: A Soft Baa
;
; Paula speaks. One channel, one eight-byte square wave in
; Chip RAM, and three farmyard sounds built from nothing but
; period (pitch), volume and time: a wobbling baa on the hop,
; a low squelch for a loss, a rising bleat at the pen. Each
; sound is two periods and a duration — the wobble between
; them is what makes a baa a baa.
;──────────────────────────────────────────────────────────────

;══════════════════════════════════════════════════════════════
; TWEAKABLE VALUES — Change these and see what happens!
;══════════════════════════════════════════════════════════════

; Colours are $0RGB (4 bits per component, values 0-F)
COLOUR_FOLD_GRASS   equ $0480       ; The fold's pasture
COLOUR_HEDGE        equ $0350       ; Hedgerow between fold and stream
COLOUR_WATER        equ $036A       ; The stream
COLOUR_BANK         equ $0350       ; Grassy bank below the stream
COLOUR_LANE         equ $0666       ; The lane's tarmac
COLOUR_VERGE        equ $0350       ; Verge below the lane
COLOUR_FIELD        equ $0470       ; The field where the flock waits

COLOUR_FENCE        equ $0531       ; Pen walls (bitplane, fold band)
COLOUR_WOOD         equ $0852       ; The footbridge (bitplane, stream band)
COLOUR_DASH         equ $0EEE       ; Lane markings (bitplane, lane band)
COLOUR_TUFT         equ $0360       ; Spare (bitplane, grass bands)

COLOUR_WOOL         equ $0EEE       ; The sheep's fleece (sprite colour 1)
COLOUR_FACE         equ $0210       ; Her face, ears and tail (sprite colour 2)
COLOUR_SHADE        equ $0BBB       ; Fleece shading (sprite colour 3)

COLOUR_TRACTOR      equ $0B20       ; The tractor's bodywork (sprite colour 1)
COLOUR_TYRE         equ $0210       ; Wheels and trim (sprite colour 2)
COLOUR_CAB          equ $0999       ; The cab roof (sprite colour 3)

COLOUR_WOODWORK     equ $0742       ; The hay cart's bed (sprites 4-5)
COLOUR_HAY          equ $0C92       ; Its heaped load

COLOUR_ROVER        equ $0364       ; The Land Rover's paint (sprites 6-7)
COLOUR_ROOF         equ $0AAA       ; Its roof panel

; Where each band begins (screen row 0-255, top to bottom)
ROW_HEDGE           equ 40
ROW_STREAM          equ 48
ROW_BANK            equ 80
ROW_LANE            equ 96
ROW_VERGE           equ 160
ROW_FIELD           equ 176

; Where the sheep starts, and how she moves
SHEEP_X             equ 152
SHEEP_Y             equ 200
STEP                equ 8           ; Pixels per hop
COOLDOWN            equ 6           ; Frames between hops

; The traffic: three lanes, three rhythms
TRACTOR_Y           equ 98          ; Top lane
TRACTOR_SPEED       equ 2           ; Steady, rightward
ROVER_Y             equ 120         ; Middle lane
ROVER_SPEED         equ -3          ; Fast, leftward
CART_Y              equ 142         ; Bottom lane — crossed first
CART_SPEED          equ -1          ; Plodding, leftward

; How long the world stops when a sheep is lost
SQUASH_BEAT         equ 25          ; Frames of stillness

; The flock
FLOCK_SIZE          equ 5           ; Sheep in hand at the start

; The fold's pens
PEN_ROW             equ 16          ; Where a resident sheep settles
FENCE_Y             equ 24          ; She stops here unless a pen is open
PEN_BEAT            equ 15          ; Frames of calm after a penning

; What things are worth
ROAD_POINTS         equ 5           ; Surviving the road (per sheep)
PEN_POINTS          equ 25          ; A sheep safely home
ROW_BANK_TOP        equ 88          ; Past here, the road is behind her

; The farmyard's voice: each sound is two periods (pitch
; slides from the first to the second halfway through), a
; volume, and a length in frames. Bigger period = lower note.
BAA_PER1            equ 1800        ; The hop: a soft baa,
BAA_PER2            equ 2100        ;   dropping as it ends
BAA_FRAMES          equ 5 
BAA_VOL             equ 40
SPLAT_PER1          equ 2800        ; The loss: low and flat,
SPLAT_PER2          equ 3600        ;   sagging lower
SPLAT_FRAMES        equ 16
SPLAT_VOL           equ 60
BLEAT_PER1          equ 1700        ; The pen: contented,
BLEAT_PER2          equ 1250        ;   rising
BLEAT_FRAMES        equ 18
BLEAT_VOL           equ 50

; The HUD strip at the foot of the screen
ROW_HUD             equ 240
COLOUR_HUD          equ $0231       ; The strip itself
COLOUR_ICON         equ $0EEE       ; Sheep icons (bitplane, HUD band)

;══════════════════════════════════════════════════════════════
; HARDWARE REGISTERS
;══════════════════════════════════════════════════════════════

CUSTOM      equ $dff000

DMACON      equ $096        ; DMA control (write)
INTENA      equ $09a        ; Interrupt enable (write)
INTREQ      equ $09c        ; Interrupt request (write)
COP1LC      equ $080        ; Copper list pointer
COPJMP1     equ $088        ; Copper restart strobe
VPOSR       equ $004        ; Beam position
JOY1DAT     equ $00c        ; Joystick, control port 2
CLXDAT      equ $00e        ; Collision data (read clears it!)
CLXCON      equ $098        ; Collision control
AUD0LC      equ $0a0        ; Audio channel 0: sample address
AUD0LEN     equ $0a4        ;   sample length (words)
AUD0PER     equ $0a6        ;   period (pitch)
AUD0VOL     equ $0a8        ;   volume (0-64)

BPLCON0     equ $100        ; Bitplane control
BPLCON1     equ $102        ; Scroll
BPLCON2     equ $104        ; Priority
BPL1MOD     equ $108        ; Odd plane modulo
DDFSTRT     equ $092        ; Display data fetch start
DDFSTOP     equ $094        ; Display data fetch stop
DIWSTRT     equ $08e        ; Display window start
DIWSTOP     equ $090        ; Display window stop
BPL1PTH     equ $0e0        ; Bitplane 1 pointer (high)
BPL1PTL     equ $0e2        ; Bitplane 1 pointer (low)
BPL2PTH     equ $0e4        ; Bitplane 2 pointer (high)
BPL2PTL     equ $0e6        ; Bitplane 2 pointer (low)
BPL2MOD     equ $10a        ; Even plane modulo
SPR0PTH     equ $120        ; Sprite 0 pointer (high)
COLOR00     equ $180        ; Background colour
COLOR01     equ $182        ; Bitplane colour 1
COLOR02     equ $184        ; Bitplane colour 2 (plane 2)
COLOR03     equ $186        ; Bitplane colour 3 (both planes)
COLOR17     equ $1a2        ; Sprite 0/1 colour 1
COLOR18     equ $1a4        ; Sprite 0/1 colour 2
COLOR19     equ $1a6        ; Sprite 0/1 colour 3
COLOR21     equ $1aa        ; Sprite 2/3 colour 1
COLOR22     equ $1ac        ; Sprite 2/3 colour 2
COLOR23     equ $1ae        ; Sprite 2/3 colour 3
COLOR25     equ $1b2        ; Sprite 4/5 colour 1
COLOR26     equ $1b4        ; Sprite 4/5 colour 2
COLOR27     equ $1b6        ; Sprite 4/5 colour 3
COLOR29     equ $1ba        ; Sprite 6/7 colour 1
COLOR30     equ $1bc        ; Sprite 6/7 colour 2
COLOR31     equ $1be        ; Sprite 6/7 colour 3

ROW_BYTES   equ 40          ; 320 pixels / 8

;══════════════════════════════════════════════════════════════
; CODE (Chip RAM — the Copper, planes and sprites live here)
;══════════════════════════════════════════════════════════════

            section code,code_c

start:
            lea     CUSTOM,a5           ; A5 = custom chip base ($DFF000)

            ; --- Take over the machine ---
            move.w  #$7fff,INTENA(a5)   ; Disable all interrupts
            move.w  #$7fff,INTREQ(a5)   ; Clear pending interrupts
            move.w  #$7fff,DMACON(a5)   ; Disable all DMA

            ; --- Point the Copper's bitplane MOVEs at our planes ---
            lea     copbpl,a1
            lea     plane,a0
            move.l  a0,d0
            move.w  d0,6(a1)            ; Low word into the BPL1PTL move
            swap    d0
            move.w  d0,2(a1)            ; High word into the BPL1PTH move
            lea     plane2,a0
            move.l  a0,d0
            move.w  d0,14(a1)           ; And the same for plane 2
            swap    d0
            move.w  d0,10(a1)

            ; --- Point sprite 0 at the sheep, the rest at nothing ---
            lea     copsprites,a1       ; Eight pointer pairs in the list
            lea     sheep0,a0
            move.l  a0,d0
            move.w  d0,6(a1)            ; Sprite 0 low word
            swap    d0
            move.w  d0,2(a1)            ; Sprite 0 high word

            lea     nullspr,a0          ; Sprites 1-7: an empty sprite
            move.l  a0,d0
            moveq   #7-1,d6
.nulls:
            lea     8(a1),a1            ; Next pointer pair in the list
            move.w  d0,6(a1)
            swap    d0
            move.w  d0,2(a1)
            swap    d0
            dbf     d6,.nulls

            ; --- ...except the traffic: sprites 2, 4 and 6.
            ; Each vehicle gets the EVEN sprite of its own pair, so
            ; each lives in its own collision group: tractor in 2/3
            ; (bit 9 against the sheep), cart in 4/5 (bit 10),
            ; Land Rover in 6/7 (bit 11) — and its own palette.
            lea     copsprites+16,a1    ; Sprite 2: the tractor
            lea     tractor,a0
            move.l  a0,d0
            move.w  d0,6(a1)
            swap    d0
            move.w  d0,2(a1)
            lea     copsprites+32,a1    ; Sprite 4: the hay cart
            lea     cart,a0
            move.l  a0,d0
            move.w  d0,6(a1)
            swap    d0
            move.w  d0,2(a1)
            lea     copsprites+48,a1    ; Sprite 6: the Land Rover
            lea     rover,a0
            move.l  a0,d0
            move.w  d0,6(a1)
            swap    d0
            move.w  d0,2(a1)


            ; --- Arm collision detection ---
            move.w  #$0000,CLXCON(a5)   ; Even sprites always take part;
                                        ;   we need nothing extra for 0-vs-2
            move.w  CLXDAT(a5),d0       ; Prime: reading clears the latches

            ; --- Draw the farmyard's detail into the bitplane ---
            bsr     drawfarmyard
            bsr     drawflock           ; The flock in hand, bottom-left
            bsr     drawscore           ; And the score, bottom-right

            ; --- Place the sheep at her starting spot ---
            bsr     updsprite

            ; --- Install Copper list ---
            lea     copperlist,a0
            move.l  a0,COP1LC(a5)
            move.w  d0,COPJMP1(a5)      ; Strobe: restart Copper from COP1LC

            ; --- Enable DMA ---
            move.w  #$83a0,DMACON(a5)   ; SET + DMAEN + BPLEN + COPEN + SPREN

            ; === Main Loop ===
mainloop:
            ; Wait for vertical blank — in two phases. If we only
            ; waited FOR line 0, a fast loop body could finish while
            ; the beam is still ON line 0 and run again in the same
            ; frame. Wait to leave line 0 first, then to reach it.
            move.l  #$1ff00,d1          ; Mask: bits 8-16 of beam position
.vbleave:
            move.l  VPOSR(a5),d0        ; Read beam position
            and.l   d1,d0               ; Isolate line number
            beq.s   .vbleave            ; Loop while still on line 0
.vbwait:
            move.l  VPOSR(a5),d0        ; Read beam position
            and.l   d1,d0               ; Isolate line number
            bne.s   .vbwait             ; Loop until line 0 again

            bsr     steer               ; Read the stick, maybe hop
            bsr     drivelanes          ; All the traffic, one mover
            bsr     checksquash         ; Did the lane win?
            bsr     soundtick           ; Wobble, and fall silent on time
            bsr     updsprite           ; Position is data: rewrite POS/CTL
            bsr     showframe           ; Point sprite 0 at this step's image

            ; Check left mouse button (active low at CIAA)
            btst    #6,$bfe001          ; CIAA Port A, bit 6
            bne.s   mainloop            ; Not pressed — keep going

            ; Button pressed — halt
.halt:
            bra.s   .halt

;══════════════════════════════════════════════════════════════
; STEER — read the joystick, hop the sheep
;
; JOY1DAT is control port 2. The decode is famously sideways:
;   right = bit 1            left = bit 9
;   down  = bit 0 XOR bit 1  up   = bit 8 XOR bit 9
; One XOR of the register with itself-shifted turns the two
; awkward pairs into plain testable bits.
;
; A hop is STEP pixels; COOLDOWN frames must pass between hops
; — that's what makes her *step* like a sheep rather than glide
; like a cursor.
;══════════════════════════════════════════════════════════════

steer:
            tst.w   won                 ; Fold full — nothing to steer
            bne.s   .frozen
            tst.w   gameover            ; No flock, no shepherd
            bne.s   .frozen
            tst.w   squashtimer         ; Mid squash-beat? She can't move
            beq.s   .alive
.frozen:    rts
.alive:
            tst.w   cooldown
            beq.s   .ready
            subq.w  #1,cooldown         ; Still mid-hop rhythm — wait
            rts
.ready:
            move.w  JOY1DAT(a5),d0      ; Read the stick
            move.w  d0,d1
            lsr.w   #1,d1
            eor.w   d0,d1               ; Now: bit 0 = down, bit 8 = up

            btst    #8,d1               ; Up?
            beq.s   .notup
            cmp.w   #FENCE_Y,sheepy     ; At the fence line?
            bgt.s   .climb
            bsr     trypen              ; Only a pen lets her past
            bra     .done
.climb:
            sub.w   #STEP,sheepy
            bra.s   .stepped
.notup:
            btst    #0,d1               ; Down?
            beq.s   .notdown
            add.w   #STEP,sheepy
            bra.s   .stepped
.notdown:
            btst    #9,d0               ; Left?
            beq.s   .notleft
            sub.w   #STEP,sheepx
            bra.s   .stepped
.notleft:
            btst    #1,d0               ; Right?
            beq     .done               ; Stick centred — no hop
            add.w   #STEP,sheepx
.stepped:
            move.w  #COOLDOWN,cooldown  ; Set the hop rhythm
            eori.w  #1,curframe         ; The other feet, next picture

            ; --- The hop has a voice ---
            move.w  #BAA_PER1,d0
            move.w  #BAA_PER2,d1
            move.w  #BAA_FRAMES,d2
            move.w  #BAA_VOL,d3
            bsr     playsound

            ; --- First time past the road? That's worth something.
            tst.w   roadflag
            bne.s   .scored
            cmp.w   #ROW_BANK_TOP,sheepy
            bgt.s   .scored
            move.w  #1,roadflag
            add.w   #ROAD_POINTS,score
            bsr     drawscore
.scored:

            ; --- Hold her inside the farm ---
            tst.w   sheepx
            bge.s   .xlow
            clr.w   sheepx
.xlow:      cmp.w   #320-16,sheepx
            ble.s   .xhigh
            move.w  #320-16,sheepx
.xhigh:     cmp.w   #FENCE_Y,sheepy     ; The fence is the ceiling;
            bge.s   .ylow               ;   pens are the only way past
            move.w  #FENCE_Y,sheepy
.ylow:      cmp.w   #ROW_HUD-16,sheepy  ; The HUD strip is not a pasture
            ble.s   .done
            move.w  #ROW_HUD-16,sheepy
.done:
            rts

;══════════════════════════════════════════════════════════════
; DRIVELANES — advance all the traffic
;
; Unit 5's mover, made data. Each vehicle is a row in vehtab —
; WHERE its x lives, and how fast it moves (signed: negative
; drives leftward). One loop walks the table: add the speed,
; wrap off whichever edge the speed points at, store. Adding a
; vehicle to the game is adding a row to the table.
;══════════════════════════════════════════════════════════════

drivelanes:
            lea     vehtab,a2
            moveq   #3-1,d6             ; Three vehicles
.veh:
            move.l  (a2)+,a0            ; A0 = where this one's x lives
            move.w  (a2)+,d1            ; D1 = its speed (signed)
            move.w  (a0),d0
            add.w   d1,d0
            tst.w   d1
            bmi.s   .leftward
            cmp.w   #320,d0             ; Rightward: clear of the right edge?
            blt.s   .store
            move.w  #-16,d0             ; Re-enter from the left
            bra.s   .store
.leftward:
            cmp.w   #-16,d0             ; Leftward: clear of the left edge?
            bgt.s   .store
            move.w  #320,d0             ; Re-enter from the right
.store:
            move.w  d0,(a0)
            dbf     d6,.veh
            rts

vehtab:     dc.l    tractx
            dc.w    TRACTOR_SPEED
            dc.l    cartx
            dc.w    CART_SPEED
            dc.l    roverx
            dc.w    ROVER_SPEED

;══════════════════════════════════════════════════════════════
; CHECKSQUASH — read the collision latches, judge the lane
;
; CLXDAT accumulates collisions as Denise draws, and READING
; IT CLEARS IT — so read it exactly once per frame and keep
; the copy. Bit 9 means "sprite 0 or 1 touched sprite 2 or 3":
; our sheep met our tractor, pixel against pixel. The hardware
; compared every overlapping pixel pair for us, for free.
;══════════════════════════════════════════════════════════════

checksquash:
            tst.w   squashtimer         ; Already mid-beat?
            beq.s   .watch
            subq.w  #1,squashtimer      ; Count the stillness down
            bne.s   .out
            move.w  CLXDAT(a5),d0       ; Beat over: flush the contact
.out:       rts                         ;   that accumulated during it
.watch:
            tst.w   gameover            ; Nothing left to lose?
            bne.s   .safe
            move.w  CLXDAT(a5),d0       ; Read once — this clears it
            and.w   #$0e00,d0           ; Bits 9/10/11: the sheep against
            beq.s   .safe               ;   ANY of the three vehicle groups
            ; --- Squashed. One fewer in hand. ---
            move.w  #SPLAT_PER1,d0
            move.w  #SPLAT_PER2,d1
            move.w  #SPLAT_FRAMES,d2
            move.w  #SPLAT_VOL,d3
            bsr     playsound
            subq.w  #1,lives
            bsr     drawflock           ; Redraw the strip
            tst.w   lives
            bgt.s   .next               ; Sheep remain — send the next one
            move.w  #1,gameover         ; The field is empty
            rts
.next:
            move.w  #SHEEP_X,sheepx     ; The next sheep steps up
            move.w  #SHEEP_Y,sheepy
            clr.w   roadflag            ; A fresh road for the next one
            move.w  #SQUASH_BEAT,squashtimer
.safe:
            rts

;══════════════════════════════════════════════════════════════
; SETPOS — pack screen (x, y) into one sprite's POS/CTL
;   a0 = sprite structure   d0 = x   d1 = y
;
; Unit 3's packing, generalised: any sprite, any position. Beam
; coordinates: VSTART = y + $2C, HSTART = x + $80 — and the
; ninth bits ride in CTL's low flags.
;══════════════════════════════════════════════════════════════

setpos:
            add.w   #$2c,d1             ; D1 = VSTART (beam line)
            move.w  d1,d2
            add.w   #16,d2              ; D2 = VSTOP (16 rows tall)
            add.w   #$80,d0             ; D0 = HSTART (beam position)

            ; POS = VSTART[7:0] << 8 | HSTART[8:1]
            move.w  d1,d3
            lsl.w   #8,d3
            move.w  d0,d4
            lsr.w   #1,d4
            and.w   #$ff,d4
            or.w    d4,d3
            move.w  d3,(a0)             ; Write POS

            ; CTL = VSTOP[7:0] << 8 | V8START<<2 | V8STOP<<1 | H0START
            move.w  d2,d3
            and.w   #$ff,d3
            lsl.w   #8,d3
            btst    #8,d1               ; VSTART's ninth bit
            beq.s   .nv8s
            or.w    #%100,d3
.nv8s:      btst    #8,d2               ; VSTOP's ninth bit
            beq.s   .nv8e
            or.w    #%010,d3
.nv8e:      btst    #0,d0               ; HSTART's odd-pixel bit
            beq.s   .nh0
            or.w    #%001,d3
.nh0:       move.w  d3,2(a0)            ; Write CTL
            rts

;══════════════════════════════════════════════════════════════
; UPDSPRITE — place every sprite for this frame
;
; One routine owns every position write: the sheep (both step
; images, so whichever showframe picks she stands in the same
; place) and the tractor.
;══════════════════════════════════════════════════════════════

updsprite:
            lea     sheep0,a0
            move.w  sheepx,d0
            move.w  sheepy,d1
            bsr     setpos
            lea     sheep1,a0
            move.w  sheepx,d0
            move.w  sheepy,d1
            bsr     setpos
            lea     tractor,a0
            move.w  tractx,d0
            move.w  #TRACTOR_Y,d1
            bsr     setpos
            lea     cart,a0
            move.w  cartx,d0
            move.w  #CART_Y,d1
            bsr     setpos
            lea     rover,a0
            move.w  roverx,d0
            move.w  #ROVER_Y,d1
            bsr     setpos
            rts

;══════════════════════════════════════════════════════════════
; SHOWFRAME — point sprite 0 at this step's image
;
; Animation is nothing but choosing which data the channel
; fetches. The Copper list's sprite 0 pointer words are
; rewritten with whichever picture curframe names — the same
; poke the startup code did, now done every frame.
;══════════════════════════════════════════════════════════════

showframe:
            lea     nullspr,a0          ; Game over, or the whole flock
            tst.w   gameover            ;   home: no sheep on the move
            bne.s   .picked
            tst.w   won
            bne.s   .picked
            lea     sheep0,a0
            tst.w   curframe
            beq.s   .picked
            lea     sheep1,a0
.picked:
            move.l  a0,d0
            lea     copsprites,a1
            move.w  d0,6(a1)            ; Sprite 0 low word
            swap    d0
            move.w  d0,2(a1)            ; Sprite 0 high word
            rts

;══════════════════════════════════════════════════════════════
; DRAW THE FARMYARD (unchanged from Unit 1)
;══════════════════════════════════════════════════════════════

drawfarmyard:
            ; --- The fold's pens (rows 4-35) ---
            moveq   #0,d0               ; x = byte 0
            moveq   #4,d1               ; row 4
            moveq   #ROW_BYTES,d2       ; full width
            moveq   #4,d3               ; 4 rows thick
            bsr     rectfill

            lea     penposts,a2         ; Post positions (byte columns)
            moveq   #6-1,d6             ; Six posts
.posts:
            moveq   #0,d0
            move.b  (a2)+,d0            ; x = next post column
            moveq   #8,d1               ; rows 8-35
            moveq   #1,d2               ; one byte wide
            moveq   #28,d3
            bsr     rectfill
            dbf     d6,.posts

            ; --- The footbridge (rows 48-79, mid-stream) ---
            moveq   #18,d0              ; byte 18 = pixel 144
            moveq   #ROW_STREAM,d1
            moveq   #4,d2               ; 32 pixels wide
            moveq   #32,d3              ; the stream's full height
            bsr     rectfill

            ; --- Lane markings: two dashed lines (rows 116, 136) ---
            moveq   #116,d1
            bsr     dashline
            move.w  #136,d1
            ; falls through

;──────────────────────────────────────────────────────────────
; dashline — a row of dashes across the lane
;   d1 = starting row. 2 bytes on, 2 bytes off, 4 rows thick.
;──────────────────────────────────────────────────────────────
dashline:
            moveq   #0,d0               ; x = byte 0
.dash:
            move.w  d1,-(sp)            ; rectfill trashes d1
            move.w  d0,-(sp)            ; ...and d0
            moveq   #2,d2               ; 2 bytes of dash
            moveq   #4,d3               ; 4 rows thick
            bsr     rectfill
            move.w  (sp)+,d0
            move.w  (sp)+,d1
            addq.w  #4,d0               ; next dash 4 bytes along
            cmp.w   #ROW_BYTES,d0
            blt.s   .dash
            rts

;──────────────────────────────────────────────────────────────
; rectfill — set a byte-aligned rectangle of pixels
;   d0 = x (bytes)   d1 = row   d2 = width (bytes)   d3 = height
;   Trashes d1, d4, d5, a0, a1.
;──────────────────────────────────────────────────────────────
rectfill:
            lea     plane,a0
            move.w  d1,d4
            mulu    #ROW_BYTES,d4       ; row * 40
            add.w   d0,d4               ; + x
            adda.w  d4,a0               ; A0 = first byte of the rectangle
            move.w  d3,d4               ; D4 = rows to go
.row:
            movea.l a0,a1
            move.w  d2,d5               ; D5 = bytes to go
.col:
            move.b  #$ff,(a1)+          ; 8 pixels on
            subq.w  #1,d5
            bne.s   .col
            lea     ROW_BYTES(a0),a0    ; down one row
            subq.w  #1,d4
            bne.s   .row
            rts

penposts:   dc.b    0,8,16,24,32,39     ; Byte columns of the six posts
            even

;══════════════════════════════════════════════════════════════
; TRYPEN — at the fence, try to enter the pen she's facing
;
; Each pentab row is a pen: the span of sheep-centre x values
; it accepts, the byte column its resident glyph is drawn at,
; and a flag byte that remembers it's taken. A hit pens her:
; the resident appears (plane 2 — white among the brown
; fences), the next sheep steps up, and a full fold wins.
;══════════════════════════════════════════════════════════════

trypen:
            move.w  sheepx,d0
            addq.w  #8,d0               ; D0 = her centre
            lea     pentab,a2
            moveq   #5-1,d6
.pen:
            cmp.w   (a2),d0             ; Left of this pen?
            blt.s   .nextpen
            cmp.w   2(a2),d0            ; Right of it?
            bgt.s   .nextpen
            tst.b   5(a2)               ; Already taken?
            bne.s   .nextpen
            ; --- She's in. A resident for the fold. ---
            move.b  #1,5(a2)
            move.w  4(a2),d0            ; Glyph byte column
            and.w   #$ff00,d0
            lsr.w   #8,d0
            bsr     penglyph
            move.w  #BLEAT_PER1,d0      ; A contented sound
            move.w  #BLEAT_PER2,d1
            move.w  #BLEAT_FRAMES,d2
            move.w  #BLEAT_VOL,d3
            bsr     playsound
            add.w   #PEN_POINTS,score   ; A sheep safely home
            bsr     drawscore
            move.w  #SHEEP_X,sheepx     ; The next sheep steps up
            move.w  #SHEEP_Y,sheepy
            clr.w   roadflag            ; Her road is still ahead of her
            move.w  #PEN_BEAT,squashtimer
            subq.w  #1,unpenned         ; A full fold wins
            bne.s   .out
            move.w  #1,won
.out:
            rts
.nextpen:
            addq.l  #6,a2
            dbf     d6,.pen
            rts                         ; Fence, post or a full pen: no way through

;──────────────────────────────────────────────────────────────
; penglyph — stamp the resident-sheep glyph into PLANE 2
;   d0 = x (bytes). Row is PEN_ROW; the glyph is the HUD icon.
;──────────────────────────────────────────────────────────────
penglyph:
            lea     plane2,a0
            move.w  #PEN_ROW,d4
            mulu    #ROW_BYTES,d4
            add.w   d0,d4
            adda.w  d4,a0
            lea     sheepicon,a2
            moveq   #8-1,d4
.row:
            move.b  (a2)+,(a0)
            lea     ROW_BYTES(a0),a0
            dbf     d4,.row
            rts

            ; Per pen: centre-x span (min, max), glyph byte
            ; column (high byte of the word), taken flag, pad
pentab:     dc.w    8,63
            dc.b    4,0
            even
            dc.w    72,127
            dc.b    12,0
            even
            dc.w    136,191
            dc.b    20,0
            even
            dc.w    200,255
            dc.b    28,0
            even
            dc.w    264,311
            dc.b    36,0
            even

;══════════════════════════════════════════════════════════════
; PLAYSOUND — start a sound on Paula channel 0
;   d0 = starting period   d1 = second period (from halfway)
;   d2 = duration (frames) d3 = volume (0-64)
;
; Paula plays a looping sample on its own DMA: point the
; channel at the wave, say how long it is and how fast to
; step through it (the period), set a volume, switch the DMA
; on. Everything after that is timing: soundtick slides the
; period at halfway and shuts the channel up when time runs
; out. New sounds steal the channel — the farmyard talks over
; itself rather than queueing politely.
;══════════════════════════════════════════════════════════════

playsound:
            lea     squarewave,a0
            move.l  a0,AUD0LC(a5)       ; The wave to loop
            move.w  #4,AUD0LEN(a5)      ; Four words = eight samples
            move.w  d0,AUD0PER(a5)      ; Pitch now...
            move.w  d1,sndper2          ; ...pitch later
            move.w  d3,AUD0VOL(a5)
            move.w  d2,sndtimer
            lsr.w   #1,d2
            move.w  d2,sndhalf          ; Where the wobble happens
            move.w  #$8001,DMACON(a5)   ; SET + AUD0EN: sing
            rts

;──────────────────────────────────────────────────────────────
; soundtick — once per frame: wobble at halfway, stop on time
;──────────────────────────────────────────────────────────────
soundtick:
            tst.w   sndtimer
            beq.s   .quiet
            subq.w  #1,sndtimer
            bne.s   .wobble
            move.w  #$0001,DMACON(a5)   ; CLR + AUD0EN: hush
            move.w  #0,AUD0VOL(a5)
            rts
.wobble:
            move.w  sndtimer,d0
            cmp.w   sndhalf,d0          ; Halfway through?
            bne.s   .quiet
            move.w  sndper2,AUD0PER(a5) ; The second note
.quiet:
            rts

;══════════════════════════════════════════════════════════════
; DRAWFLOCK — the sheep in hand, as icons on the HUD strip
;
; One 8x8 glyph per sheep still in hand, drawn at the bottom
; left; the strip is cleared first so a lost sheep disappears.
; The icons are bitplane pixels — the HUD band's COLOR01 makes
; them white, the same per-band trick as the fence and dashes.
;══════════════════════════════════════════════════════════════

drawflock:
            ; Clear the icon area (a row of byte-rectangles)
            moveq   #1,d0               ; From byte 1
            move.w  #ROW_HUD+4,d1
            moveq   #12,d2              ; Room for the whole flock
            moveq   #8,d3
            bsr     rectclear

            ; One glyph per sheep in hand
            move.w  lives,d7
            ble.s   .none               ; Empty hand, empty strip
            moveq   #1,d6               ; First icon at byte 1
.icons:
            move.w  d6,d0
            move.w  #ROW_HUD+4,d1
            lea     sheepicon,a2
            bsr     drawglyph
            addq.w  #2,d6               ; Two bytes along for the next
            subq.w  #1,d7
            bne.s   .icons
.none:
            rts

;──────────────────────────────────────────────────────────────
; drawglyph — copy an 8-row, 1-byte-wide glyph into the plane
;   d0 = x (bytes)   d1 = row   a2 = glyph (8 bytes)
;   Trashes d1, d4, a0.
;──────────────────────────────────────────────────────────────
drawglyph:
            lea     plane,a0
            move.w  d1,d4
            mulu    #ROW_BYTES,d4
            add.w   d0,d4
            adda.w  d4,a0
            moveq   #8-1,d4
.row:
            move.b  (a2)+,(a0)
            lea     ROW_BYTES(a0),a0
            dbf     d4,.row
            rts

;──────────────────────────────────────────────────────────────
; rectclear — rectfill's opposite: clear a byte-aligned block
;   d0 = x (bytes)   d1 = row   d2 = width (bytes)   d3 = height
;   Trashes d1, d4, d5, a0, a1.
;──────────────────────────────────────────────────────────────
rectclear:
            lea     plane,a0
            move.w  d1,d4
            mulu    #ROW_BYTES,d4
            add.w   d0,d4
            adda.w  d4,a0
            move.w  d3,d4
.row:
            movea.l a0,a1
            move.w  d2,d5
.col:
            clr.b   (a1)+
            subq.w  #1,d5
            bne.s   .col
            lea     ROW_BYTES(a0),a0
            subq.w  #1,d4
            bne.s   .row
            rts

sheepicon:  dc.b    %00100100           ; A sheep, in eight bytes:
            dc.b    %01111110           ;   ears up top,
            dc.b    %11111111           ;   a fat woolly middle,
            dc.b    %11111111
            dc.b    %11111111
            dc.b    %01111110
            dc.b    %00111100           ;   tapering to
            dc.b    %00011000           ;   a little tail
            even

;══════════════════════════════════════════════════════════════
; DRAWSCORE — four decimal digits at the strip's right end
;
; The score lives as one binary word; the display is decimal.
; DIVU by 10 peels the digits off the right: the remainder is
; the next digit, the quotient carries on. Stamp them right to
; left with the same drawglyph the icons use — each digit is
; just a glyph in the font table.
;══════════════════════════════════════════════════════════════

drawscore:
            move.w  score,d7            ; D7 = what's left to convert
            moveq   #38,d5              ; Rightmost digit's byte column
            moveq   #4-1,d6             ; Four digits
.digit:
            moveq   #0,d0
            move.w  d7,d0
            divu    #10,d0              ; Quotient low, remainder high
            move.l  d0,d1
            swap    d1                  ; D1 = this digit (0-9)
            move.w  d0,d7               ; D7 = the rest
            ; Find the digit's glyph: font + digit*8
            lea     digitfont,a2
            add.w   d1,d1
            add.w   d1,d1
            add.w   d1,d1               ; digit * 8
            adda.w  d1,a2
            move.w  d5,d0               ; Byte column
            move.w  #ROW_HUD+4,d1
            bsr     drawglyph
            subq.w  #1,d5               ; Next digit to the left
            dbf     d6,.digit
            rts

digitfont:  ; 0-9, one byte per row, eight rows each
            dc.b    %01111100,%11000110,%11001110,%11010110,%11100110,%11000110,%01111100,0  ; 0
            dc.b    %00011000,%00111000,%00011000,%00011000,%00011000,%00011000,%01111110,0  ; 1
            dc.b    %01111100,%11000110,%00000110,%00111100,%01100000,%11000000,%11111110,0  ; 2
            dc.b    %01111100,%11000110,%00000110,%00111100,%00000110,%11000110,%01111100,0  ; 3
            dc.b    %00011100,%00111100,%01101100,%11001100,%11111110,%00001100,%00001100,0  ; 4
            dc.b    %11111110,%11000000,%11111100,%00000110,%00000110,%11000110,%01111100,0  ; 5
            dc.b    %01111100,%11000000,%11111100,%11000110,%11000110,%11000110,%01111100,0  ; 6
            dc.b    %11111110,%00000110,%00001100,%00011000,%00110000,%00110000,%00110000,0  ; 7
            dc.b    %01111100,%11000110,%01111100,%11000110,%11000110,%11000110,%01111100,0  ; 8
            dc.b    %01111100,%11000110,%11000110,%01111110,%00000110,%00000110,%01111100,0  ; 9
            even

;══════════════════════════════════════════════════════════════
; COPPER LIST — the farmyard, plus eight sprite pointers
;══════════════════════════════════════════════════════════════

copperlist:
            ; --- Display setup ---
            dc.w    DIWSTRT,$2c81       ; Window: top-left
            dc.w    DIWSTOP,$2cc1       ; Window: bottom-right
            dc.w    DDFSTRT,$0038       ; Fetch start (lores)
            dc.w    DDFSTOP,$00d0       ; Fetch stop
            dc.w    BPLCON0,$2200       ; 2 bitplanes, colour burst on
            dc.w    BPLCON1,$0000       ; No scroll
            dc.w    BPLCON2,$0024       ; Sprites in front of playfield
            dc.w    BPL1MOD,$0000       ; No modulo — rows pack tight
            dc.w    BPL2MOD,$0000
copbpl:
            dc.w    BPL1PTH,$0000       ; Plane addresses, poked in
            dc.w    BPL1PTL,$0000       ;   by the CPU at startup
            dc.w    BPL2PTH,$0000
            dc.w    BPL2PTL,$0000

copsprites:
            dc.w    SPR0PTH+0,$0000     ; Sprite 0: the sheep (poked in)
            dc.w    SPR0PTH+2,$0000
            dc.w    SPR0PTH+4,$0000     ; Sprites 1-7: the null sprite
            dc.w    SPR0PTH+6,$0000
            dc.w    SPR0PTH+8,$0000
            dc.w    SPR0PTH+10,$0000
            dc.w    SPR0PTH+12,$0000
            dc.w    SPR0PTH+14,$0000
            dc.w    SPR0PTH+16,$0000
            dc.w    SPR0PTH+18,$0000
            dc.w    SPR0PTH+20,$0000
            dc.w    SPR0PTH+22,$0000
            dc.w    SPR0PTH+24,$0000
            dc.w    SPR0PTH+26,$0000
            dc.w    SPR0PTH+28,$0000
            dc.w    SPR0PTH+30,$0000

            ; --- Plane-2 colours: a resident sheep is white wherever
            ;     she settles (colour 2 = plane 2 alone; colour 3 = both
            ;     planes — fence-and-sheep never overlap, white is safe)
            dc.w    COLOR02,$0EEE
            dc.w    COLOR03,$0EEE

            ; --- The sheep's colours (sprites 0-1 share 17-19) ---
            dc.w    COLOR17,COLOUR_WOOL
            dc.w    COLOR18,COLOUR_FACE
            dc.w    COLOR19,COLOUR_SHADE

            ; --- The tractor's colours (sprites 2-3 share 21-23) ---
            dc.w    COLOR21,COLOUR_TRACTOR
            dc.w    COLOR22,COLOUR_TYRE
            dc.w    COLOR23,COLOUR_CAB

            ; --- The hay cart's (sprites 4-5 share 25-27) ---
            dc.w    COLOR25,COLOUR_WOODWORK
            dc.w    COLOR26,COLOUR_TYRE
            dc.w    COLOR27,COLOUR_HAY

            ; --- The Land Rover's (sprites 6-7 share 29-31) ---
            dc.w    COLOR29,COLOUR_ROVER
            dc.w    COLOR30,COLOUR_TYRE
            dc.w    COLOR31,COLOUR_ROOF

            ; --- THE FOLD (from the top of the frame) ---
            dc.w    COLOR00,COLOUR_FOLD_GRASS
            dc.w    COLOR01,COLOUR_FENCE        ; Pixels here are fence

            ; --- HEDGEROW (row 40) ---
            dc.w    $5401,$fffe                 ; Wait: line $2C+40 = $54
            dc.w    COLOR00,COLOUR_HEDGE
            dc.w    COLOR01,COLOUR_TUFT

            ; --- THE STREAM (row 48) ---
            dc.w    $5c01,$fffe                 ; Wait: line $2C+48 = $5C
            dc.w    COLOR00,COLOUR_WATER
            dc.w    COLOR01,COLOUR_WOOD         ; Pixels here are bridge

            ; --- THE BANK (row 80) ---
            dc.w    $7c01,$fffe                 ; Wait: line $2C+80 = $7C
            dc.w    COLOR00,COLOUR_BANK
            dc.w    COLOR01,COLOUR_TUFT

            ; --- THE LANE (row 96) ---
            dc.w    $8c01,$fffe                 ; Wait: line $2C+96 = $8C
            dc.w    COLOR00,COLOUR_LANE
            dc.w    COLOR01,COLOUR_DASH         ; Pixels here are markings

            ; --- THE VERGE (row 160) ---
            dc.w    $cc01,$fffe                 ; Wait: line $2C+160 = $CC
            dc.w    COLOR00,COLOUR_VERGE
            dc.w    COLOR01,COLOUR_TUFT

            ; --- THE FIELD (row 176, down to row 239) ---
            dc.w    $dc01,$fffe                 ; Wait: line $2C+176
            dc.w    COLOR00,COLOUR_FIELD
            dc.w    COLOR01,COLOUR_TUFT

            ; --- THE HUD STRIP (row 240) ---
            ; Row 240 is beam line $11C — past 255, which the Copper's
            ; 8-bit comparator can't name directly. The classic trick:
            ; wait for the very end of line 255, THEN wait for the low
            ; byte. The first wait carries you across the boundary.
            dc.w    $ffdf,$fffe                 ; To the end of line 255
            dc.w    $1c01,$fffe                 ; Then line $11C & $FF = $1C
            dc.w    COLOR00,COLOUR_HUD
            dc.w    COLOR01,COLOUR_ICON

            ; --- END OF COPPER LIST ---
            dc.w    $ffff,$fffe                 ; Wait for impossible position

;══════════════════════════════════════════════════════════════
; THE SHEEP — sprite 0, two step images
;
; Same sheep, two pictures. Step image 0: front-left and
; back-right feet planted. Step image 1: the other diagonal,
; tail swung the other way. Alternate them as she hops and
; she waddles. The control words are written by updsprite.
;══════════════════════════════════════════════════════════════

            section data,data_c

sheep0:
            dc.w    0                   ; POS — written by updsprite
            dc.w    0                   ; CTL — written by updsprite

            ;        plane A (fleece)    plane B (face/shade/feet)
            dc.w    %0000000000000000,%0000100000010000  ; ..ears..
            dc.w    %0000000000000000,%0000011111100000  ; ..head..
            dc.w    %0000000000000000,%0000001111000000  ; ..face..
            dc.w    %0000111111110000,%0000000000000000  ; fleece ruff
            dc.w    %0011111111111100,%0000000000000000  ; shoulders
            dc.w    %0111111111111110,%1000000000000000  ; < front foot
            dc.w    %0111111111111110,%1001000000001000  ; < + flecks
            dc.w    %0111111111111110,%0000000000000000
            dc.w    %0111111111111110,%0000001001000000  ; shade flecks
            dc.w    %0111111111111110,%0000000000000001  ; back foot >
            dc.w    %0111111111111110,%0000100000010001  ; + flecks  >
            dc.w    %0011111111111100,%0000000000000000  ; haunches
            dc.w    %0011111111111100,%0000000000000000
            dc.w    %0001111111111000,%0000000000000000
            dc.w    %0000111111110000,%0000000000000000  ; rump
            dc.w    %0000000000000000,%0000001100000000  ; tail, left

            dc.w    0,0                 ; End of sprite

sheep1:
            dc.w    0                   ; POS — written by updsprite
            dc.w    0                   ; CTL — written by updsprite

            ;        plane A (fleece)    plane B (face/shade/feet)
            dc.w    %0000000000000000,%0000100000010000  ; ..ears..
            dc.w    %0000000000000000,%0000011111100000  ; ..head..
            dc.w    %0000000000000000,%0000001111000000  ; ..face..
            dc.w    %0000111111110000,%0000000000000000  ; fleece ruff
            dc.w    %0011111111111100,%0000000000000000  ; shoulders
            dc.w    %0111111111111110,%0000000000000001  ; front foot >
            dc.w    %0111111111111110,%0001000000001001  ; + flecks  >
            dc.w    %0111111111111110,%0000000000000000
            dc.w    %0111111111111110,%0000001001000000  ; shade flecks
            dc.w    %0111111111111110,%1000000000000000  ; < back foot
            dc.w    %0111111111111110,%1000100000010000  ; < + flecks
            dc.w    %0011111111111100,%0000000000000000  ; haunches
            dc.w    %0011111111111100,%0000000000000000
            dc.w    %0001111111111000,%0000000000000000
            dc.w    %0000111111110000,%0000000000000000  ; rump
            dc.w    %0000000000000000,%0000000011000000  ; tail, right

            dc.w    0,0                 ; End of sprite

;══════════════════════════════════════════════════════════════
; THE TRACTOR — sprite 2
;
; Big rear wheels on the left, small front wheels and the
; bonnet pointing right — the way it drives. Red bodywork,
; dark tyres, a grey cab roof. Its own palette: sprites 2-3
; share colours 21-23.
;══════════════════════════════════════════════════════════════

tractor:
            dc.w    0                   ; POS — written by updsprite
            dc.w    0                   ; CTL — written by updsprite

            ;        plane A (body/cab)   plane B (tyres/cab)
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0111110000000000  ; rear wheel
            dc.w    %0000000000000000,%0111110000011110  ; + front wheel
            dc.w    %0000000000000000,%0111110000011110
            dc.w    %0000001111111100,%0111110000000000  ; chassis
            dc.w    %0111111111111110,%0000111110000000  ; body + cab
            dc.w    %0111111111111110,%0000111110000000
            dc.w    %0111111111111110,%0000111110000000
            dc.w    %0111111111111110,%0000111110000000
            dc.w    %0000001111111100,%0111110000000000  ; chassis
            dc.w    %0000000000000000,%0111110000011110
            dc.w    %0000000000000000,%0111110000011110  ; + front wheel
            dc.w    %0000000000000000,%0111110000000000  ; rear wheel
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0000000000000000

            dc.w    0,0                 ; End of sprite

nullspr:    dc.w    0,0                 ; A sprite that displays nothing
            dc.w    0,0

; --- The sheep's state ---
sheepx:     dc.w    SHEEP_X             ; Screen x (0-304)
sheepy:     dc.w    SHEEP_Y             ; Screen y (0-240)
cooldown:   dc.w    0                   ; Frames until the next hop
curframe:   dc.w    0                   ; Which step image: 0 or 1

;══════════════════════════════════════════════════════════════
; THE HAY CART — sprite 4
;
; Plods leftward: a wooden bed, a heaped load of hay, and
; wheels at the corners. Wood and hay get their own palette
; (sprites 4-5 share colours 25-27).
;══════════════════════════════════════════════════════════════

cart:
            dc.w    0                   ; POS — written by updsprite
            dc.w    0                   ; CTL — written by updsprite

            ;        plane A (wood/hay)   plane B (wheels/hay)
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0110000000000110  ; wheels
            dc.w    %0011111111111100,%0110000000000110
            dc.w    %0011111111111100,%0110000000000110
            dc.w    %0011111111111100,%0000111111110000  ; hay rises
            dc.w    %0011111111111100,%0001111111111000
            dc.w    %0011111111111100,%0001111111111000
            dc.w    %0011111111111100,%0001111111111000
            dc.w    %0011111111111100,%0001111111111000
            dc.w    %0011111111111100,%0000111111110000  ; hay falls
            dc.w    %0011111111111100,%0110000000000110
            dc.w    %0011111111111100,%0110000000000110  ; wheels
            dc.w    %0000000000000000,%0110000000000110
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0000000000000000

            dc.w    0,0                 ; End of sprite

;══════════════════════════════════════════════════════════════
; THE LAND ROVER — sprite 6
;
; The farmer's in a hurry. Boxy paintwork, a roof panel set
; back from the bonnet (it drives leftward, so the bonnet is
; the left end), wheels at the corners. Sprites 6-7 share
; colours 29-31.
;══════════════════════════════════════════════════════════════

rover:
            dc.w    0                   ; POS — written by updsprite
            dc.w    0                   ; CTL — written by updsprite

            ;        plane A (paint/roof)  plane B (wheels/roof)
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0011000000001100  ; wheels
            dc.w    %0111111111111110,%0011000000001100
            dc.w    %0111111111111110,%0011000000001100
            dc.w    %0111111111111110,%0000001111111000  ; roof panel,
            dc.w    %0111111111111110,%0000001111111000  ;   set back
            dc.w    %0111111111111110,%0000001111111000  ;   from the
            dc.w    %0111111111111110,%0000001111111000  ;   bonnet
            dc.w    %0111111111111110,%0000001111111000
            dc.w    %0111111111111110,%0000001111111000
            dc.w    %0111111111111110,%0011000000001100
            dc.w    %0111111111111110,%0011000000001100  ; wheels
            dc.w    %0000000000000000,%0011000000001100
            dc.w    %0000000000000000,%0000000000000000
            dc.w    %0000000000000000,%0000000000000000

            dc.w    0,0                 ; End of sprite

; --- The traffic's state ---
tractx:     dc.w    -16                 ; The tractor enters from the left
cartx:      dc.w    300                 ; The cart from the right
roverx:     dc.w    160                 ; The Rover mid-lane, flat out

; --- The squash beat ---
squashtimer: dc.w   0                   ; Frames of stillness remaining

; --- The flock ---
lives:      dc.w    FLOCK_SIZE          ; Sheep in hand
gameover:   dc.w    0                   ; 1 = the field is empty
won:        dc.w    0                   ; 1 = every pen is full
unpenned:   dc.w    5                   ; Pens still to fill
score:      dc.w    0                   ; Points so far
roadflag:   dc.w    0                   ; This sheep has crossed the road

; --- The voice ---
sndtimer:   dc.w    0                   ; Frames of sound remaining
sndhalf:    dc.w    0                   ; When to slide the pitch
sndper2:    dc.w    0                   ; The pitch to slide to

squarewave: dc.b    64,64,64,64,-64,-64,-64,-64   ; One cycle, eight samples
            even

;══════════════════════════════════════════════════════════════
; THE BITPLANE (Chip RAM)
;══════════════════════════════════════════════════════════════

plane:      ds.b    ROW_BYTES*256       ; Plane 1: fence, bridge, dashes, icons
plane2:     ds.b    ROW_BYTES*256       ; Plane 2: the fold's residents

If It Doesn’t Work

  • Silence? Audio DMA is its own enable: $8001 to DMACON (SET + AUD0EN) at trigger time — the display’s $83A0 doesn’t cover it. And the wave must live in Chip RAM (data_c); Paula can’t fetch from anywhere else.
  • A sound that never stops? soundtick must run every frame and must clear the channel ($0001 — CLR + AUD0EN) when the timer dies. A forgotten volume write leaves a faint whine even with DMA off.
  • Pops and clicks at sound boundaries? The wave is interrupted mid-cycle when a new sound steals the channel — at these volumes it reads as farmyard texture, but the experiment with longer waves will expose it. Fading volume over the last frames is the classic cure.
  • Wrong pitches everywhere? Period is samples-per-step in clock ticks: PAL Paula steps at 3,546,895 ticks/sec, and our wave is 8 samples long — so frequency = 3546895 ÷ (period × 8). Check your arithmetic against a pitch you trust.

Try This

  1. A near-miss whistle. Unit 6’s Try This built a near-miss detector; give it a voice — short, high, quiet. Suddenly the lane taunts.
  2. Channel 1. Move the world’s sounds (squelch) to AUD1LC/AUD1PER/AUD1VOL ($0B0–$0B8, DMA bit 1) and keep the sheep on channel 0. Give each channel its own timer, halfway count and later period as well. Two hardware voices still need separate playback state and a rule for requests that target the same voice.
  3. The win deserves a fanfare. Three notes, rising, on the won transition — three playsound calls won’t work (each steals the last). What’s the smallest change to soundtick that plays a sequence? Sketch it; Unit 17’s title music will want your answer.

What You’ve Learnt

  • Paula’s model — looping samples from Chip RAM on per-channel DMA: address, length, period, volume, enable. Tones are the shortest possible sample.
  • Period arithmetic — pitch = clock ÷ (period × sample length); bigger period, lower note.
  • Two-note sound design — a pitch slide at halfway gives each event its emotional shape; down deflates, up delights.
  • Channel stealing — one channel needs an explicit replacement policy; newest-wins and priority preserve different feedback.
  • Sound bolts onto events — clean event points from Units 3–9 made the voice a four-line addition at each site.

What’s Next

That’s Arc 1’s systems complete: the farmyard, a steerable animated sheep, deterministic traffic, collision, lives, the fold, score and sound — a whole road-crossing game. What remains for the arc is making it finish properly: levels that escalate, the clean-level bonus, and the game-over and victory beats — the polish pass where it stops being systems and becomes a thing you’d hand someone.

Run it here

This unit's program, built for you.

Program
flock.adf
Machine
Commodore Amiga 500 + A501 trapdoor (OCS PAL)

Download flock.adf