# Behaviour divergence: quack vs duck-pond

A line-by-line comparison of the behaviour model in **quack** (the 3-D
TypeScript pond, `src/pond.ts`) against **duck-pond** (the 2-D Rust audio
pond, `duck-pond/src/scene.rs`), asking three questions:

1. What does the Rust model do that quack **has not ported** yet?
2. Where has quack taken **creative freedom** — invented behaviour the Rust
   never had?
3. Where does the behaviour **differ because of the two things quack has that
   the Rust cannot**: the **flight** animation and the **winter** freeze?

`docs/port-notes.md` is the working record of *how* the port was done. This
file is the *balance sheet* — what came across, what did not, what was added,
and what changed for a reason.

The comparison treats both projects as read-only. quack's only change to the
Rust tree is the additive audio `Stage` described below; all other claims are
about the original.

**Update.** The calling model was ported as behaviour first: the nine
intervals, sleeping suppression, hen lottery, brood calls and one-hop answer
chain live in `src/pond.ts`, tuned by `CALLS` in `src/config.ts`. The audio
stack has since followed (v2): the vendored Rust synthesis, propagation and
spatialization run from `src/audio.ts`, so the same `CallEvent`s that opened a
silent head/bill gesture now also play. `docs/audio.md` maps every Rust sound
to the quack action that makes it. Sections 2.1–2.5 and 4.4 have been rewritten
to record what closed; the dropped appendix keeps the sound-by-sound list. Line
numbers in those rewritten sections are from the tree before the call port.

**Conventions.** quack's own files are always written with their `src/` or
`docs/` prefix (`src/pond.ts:611`). The Rust originals are quoted by their
bare file name — `scene.rs`, `synth.rs`, `main.rs` — and always mean the
vendored copy under `duck-pond/src/`. Line numbers are from the working tree
at the time of writing (`src/pond.ts` 1643 lines, `src/duck.ts` 1423,
`src/config.ts` 538, `src/world.ts` 1330, `scene.rs` 1494). This file uses the
British "behaviour" the quack source uses; the Rust source uses "behaviour"
too in its comments, and the Rust *docs* use "behavior" in the README.

---

## 1. The scorecard

| | duck-pond (Rust, 2-D, audio) | quack (TypeScript, 3-D, visual) |
| --- | --- | --- |
| Activities | 6: `Swim Dabble Rest Preen Stray Feed` | 8: `swim dabble rest preen stray feed` **+ `beg` `fly`** |
| Sub-actions (poses) | none — a duck is a letter in a map | 12: `sleep stretch look idle preen peck dabble upend drink chirp beg` (+ `startle`, dead) |
| Calling model | 9 inter-call intervals, a 4-way call lottery, a social flag, a one-hop answer chain | **ported** — the same timing, lottery and chain; the same Rust voices, now wired to the stage |
| Food | 12–20 peas per throw, one peck each | one crust per throw, 2–3 bites, plus begging |
| Water | one medium, deep enough to swim | three: swim (0.3 m), wade (0.05 m), walk the ice lid (0.5 frozen) |
| Year | none — a timeless breeding pond | four seasons, 28 min a year, ice from every edge |
| Locomotion | swim, waddle | swim, waddle, **lap**, **fly** |
| Depth bands | 3–10 / 10–25 / 25–46 m from the listener | world-z bands 22–38 / 0–22 / −38…4 |
| Personal space | a 0.6 m radius nudge for adults | a per-duck ellipse hull, family vs stranger gaps, and a hard separation pass |
| RNG | ChaCha12 (`StdRng`), `seed_from_u64` | mulberry32, `seed ^ 0x51ab`, plus a private stream per duck |

The **decision structure came across essentially intact**. Every transition
weight, every bout length, every family rule and every speed constant is
present in the TS with the same value. The vocal half is present too, in
timing *and* sound: a call is both a head/bill gesture and a `CallEvent` on
`Pond.onCall`, and `AudioCore` sends that event to the Rust stage. What is new
is a whole visual and seasonal layer.

---

## 2. Part A — The calling model and remaining gaps

### 2.1 The calling model — ported, and speaking

The Rust's `Scene::call()` (`scene.rs:1189-1292`) is roughly a third of the
whole simulation. **Its timing half is ported** into `src/pond.ts`
(`tickCalls`, `callInterval`, `call`, `showCall`), tuned by `CALLS` in
`src/config.ts`. Its sound half is the Rust itself: the wasm `Stage` adds a
manual layer to `scene.rs` (kept as `audio/duck-pond-stage.patch`), and
`AudioCore` in `src/audio.ts` feeds it each `CallEvent` — `{ kind, label,
notes, duration, social, answering }` — with the caller and its voice.

The ported pieces:

| Rust behaviour | Where | Status in quack |
| --- | --- | --- |
| Nine inter-call intervals, armed in precedence order | `scene.rs:1201-1211` | **ported** — `CALLS.interval`, same precedence |
| Sleeping ducks swallow 80–90 % of their calls | `scene.rs:1213-1216` | **ported** — `CALLS.sleepSkip` |
| The hen's call lottery: 45 % single quack / 25 % social series / 20 % decrescendo / 10 % chatter, with a 60 % feeding branch in front of it | `scene.rs:1219-1249` | **ported** — same branches, `CallKind` names |
| The `social` flag — only the series and the decrescendo can set others off | `scene.rs:1241, 1244` | **ported** — `CallEvent.social` |
| The answer chain: mate p = 0.6, others `0.3·(1 − dist/40)`, within 40 m, one hop only | `scene.rs:1278-1291` | **ported** — `CALLS.answer`, `event.answering` closes the chain |
| A resting duck woken by a neighbour answers | `scene.rs:1198` | **ported** — `answering` overrides the sleep suppression |
| The drake's "mate out of reach" rule (8 m, 8–20 s instead of 45–180 s) | `scene.rs:1196, 1207-1208` | **ported** — `CALLS.mateAway`, `CALLS.interval.drakeAway` |
| The hen's brood call and its 3.7 notes/s rate | `scene.rs:1229-1233`, `synth.rs:518-530` | **ported** — `CALLS.broodNotes`, `CallEvent.duration` |

`CallEvent.duration` uses the same note-count formulas as `synth.rs`, so the
stage can start a call at the right moment and fill in the same envelope — the
`kind`/`notes`/`duration` triple is exactly the call's `synth.rs` recipe. The
gesture and the sound are offset only by the worker queue (`AUDIO.queue`, about
85 ms). The timing, social chain and brood recall are behaviour, and they are
in the pond; the voices are the vendored Rust. The mirror rule (§3.12) is no
longer the only source of the pond's lively-and-drowsy stretches.

### 2.2 `gathering` — now read by the brood call

`gathering` is set when a hen with young starts a swim bout, and it is now
read by the brood branch of `call()`. A gathering hen gives the 3–6-note
`kwek-kwek`; a hen with lost young gives the 6–10-note version. Both cue any
*stray* duckling back to her. The flag is cleared when the call fires, so one
gathering makes one call.

The visible gesture is the same `chirp` head/bill lift used by every call; the
sound is the stage's brood call beside it. It deliberately does not borrow the
`beg` pose: `beg` is an activity pose, and starting it from the call would
interrupt whatever the hen was really doing.

### 2.3 Two duckling states — both present

The Rust distinguishes two duckling calls with quite different meanings
(`scene.rs:1255-1256`):

- **`peep peep`** — loud distress whistles, only when the duckling is *lost*
  (`kind: 'peeps'`, repeated every 0.2–1 s);
- **`pip-pip`** — soft contact notes, otherwise (`kind: 'contact'`, 2–5 notes,
  every 4–15 s, or 1.5–5 s while feeding).

The old random 0.3 Hz lost chirp in `updateRig` has been removed; the call
scheduler now owns the gesture, so a content duckling is no longer silent and a
lost one peeps at the Rust's pace.

### 2.4 The duckling "peep → mother answers → return" chain — complete

The Rust's chain is: a lost duckling peeps → the mother's call timer is cut to
0.5–2.5 s (`scene.rs:1265-1269`) → she gives a brood call → every *stray*
duckling is cued back to her (`scene.rs:1270-1277`).

All three steps are now in `call()` and `tickCalls()`. A lost peep shortens the
mother's timer, her next call is forced into the brood branch by `lostYoung`,
and the stray callbacks run while the call is being made. The separate
feed-time `cue` remains as a safety net; the call is now the normal way home.

### 2.5 The dabbling splash rhythm and the bill clack

The Rust schedules a **sound** for dabbling — a 2–7 s patter of tiny splashes
every 3–15 s while an adult feeds on the water (`scene.rs:884-886`,
`950-960`) — and a clack-and-swallow when a duck eats a pea
(`scene.rs:897-903`). These are now wired rather than dropped:

- `Duck.startAction('dabble')` emits `onDabble(d, seconds)`; `Pond` forwards it
  to `AudioCore.dabble`, which calls the Rust `dabbling` generator at the
  duck's place. Only an adult on water starts a `dabble` action; grazing on the
  bank selects `peck`/`look`/`idle` instead, so the Rust's "ashore is silent"
  rule survives by construction (`src/duck.ts:925-935`).
- A bill that lands on bread fires `Duck.onPeck`; `Pond.peck` forwards it only
  when the duck is actually eating (`d.eating > 0`), with `onWater` and a
  duckling flag. Grazing pecks are silent. The Rust `peck` generator makes the
  clacks, the splash/lid difference and the gulp.
- The pea plop/tap is the bread landing: `Pond.onBreadLand` sends the Rust
  `bubble` or `tap` at the landing point, with `AUDIO.bread` gain. The Rust's
  floating-pea ring has no audio counterpart; only the landing does.

The behavioural rule is still reproduced: a duck on the bank pecks at the grass
rather than dabbling (`src/duck.ts:925-935`), and its pecks on bank food are
the dry `peck`, not a splash.

### 2.6 The Rust's "restore" bug — and the port's fix

This is the one place where the two projects **disagree about the original's
behaviour**, and quack is right.

The Rust's `stop_feeding` (`scene.rs:699-717`) does **not** restore what a
duck was doing before the peas arrived. A leader gets a brand-new activity
(50 % Dabble / 25 % Rest / 25 % Preen) with a fresh 60–180 s bout; a follower
is re-cued from its leader. `README.md:29` in the Rust project claims "Once the
peas are gone, they go back to what they were doing", and that claim is false
— a discrepancy documented in `duck-pond/behaviors.md` §7.1.

quack **does** restore it:

```ts
// src/pond.ts:1056-1069
private resumeAfterFeed(d: Duck): void {
    const pre = d.preFeed;
    d.preFeed = null;
    ...
    if (pre && pre.activity !== 'feed' && pre.activity !== 'beg') {
        d.setActivity(pre.activity);
        d.activityLeft = this.boutLength(pre.activity);
    } else {
        const r = this.rng.next();
        const a: Activity = r < 0.5 ? 'dabble' : r < 0.75 ? 'rest' : 'preen';
        d.setActivity(a);
        d.activityLeft = this.rng.range(FLOCK.bout.afterFeed.min, FLOCK.bout.afterFeed.max);
    }
```

`d.preFeed` is captured when a duck notices the bread (`src/pond.ts:624`) and
restored here, with the Rust's 50/25/25 lottery kept as the fallback for a duck
that was not doing anything in particular. The port matches the README and the
original does not. **The Rust is the one that should be fixed.**

### 2.7 Things the Rust has that were *replaced* rather than dropped

These are not gaps so much as substitutions — the TS reached for a different
mechanism for the same job.

| Rust | quack's replacement |
| --- | --- |
| `PERSONAL_SPACE = 1.0` m from the listener (`scene.rs:79-80`) | a per-duck ellipse hull with family and stranger gaps (`src/config.ts:453-464`, `src/pond.ts:1097-1183`) |
| Metric distance bands, 3–10 / 10–25 / 25–46 m (`scene.rs:95-99`) | world-z bands, `near` 22–38 / `mid` 0–22 / `far` −38…4 (`src/config.ts:155-157`) — a different axis entirely, because the listener is a stand point in a 3-D scene, not an origin |
| Adults separate within 0.6 m (`scene.rs:1121-1128`) | `SPACE.reach` 1.3× the hull distance, up to 0.28 m/s, plus two hard settling passes (`src/pond.ts:1127-1183`) |
| `bank_point` on a 18 × 7.5 m strip (`scene.rs:77-78, 117-124`) | a 52 m-wide beach, z 46.6–51.5, at least 2 m (5 m for a hen with young) from the thrower (`src/config.ts:143`, `src/pond.ts:562-569`) |
| Two regions: `in_pond` and `on_bank` (`scene.rs:82-93`) | three media: swim, wade, walk the lid (`src/pond.ts:132-148`, `src/config.ts:171`) |
| The shore-slide fallback (full / x / y / tangential step, `scene.rs:1166-1180`) | **ported** — the same four-step fallback (`src/pond.ts:1317-1344`) |
| `stop_feeding` for a follower → `cue(i)` (`scene.rs:701-704`) | `resumeAfterFeed` → `followLead` (`src/pond.ts:1072`) |

### 2.8 The RNG

The two ponds are **not** bit-compatible, and cannot be. The Rust uses
`rand`'s ChaCha12 via `StdRng::seed_from_u64` (`main.rs:140`);
quack uses mulberry32 (`src/utils.ts:56-93`) seeded with `seed ^ 0x51ab`
(`src/pond.ts:66`), with a private stream derived per duck
(`src/duck.ts:726`).

This is not a porting failure — it is unavoidable, since the two programs draw
different numbers of randoms per frame (quack spends draws on laps, flights,
bread, begging and the sub-action layer; the Rust spends them on calls,
bubbles and the water's normal samples). But it has one consequence worth
recording: **quack's per-duck private streams make the roster order
load-bearing.** Change anything drawn before the ducklings and every later
duck's spawn position and body scale moves (`src/pond.ts:111-116`).

There is also one unseeded escape hatch: `between(min, max)` uses raw
`Math.random()` (`src/utils.ts:15-18`), used for three cosmetic bread-ring
values (`src/pond.ts:1466, 1555, 1559-1560`). These never feed back into a
duck decision, but they mean a seeded quack is not perfectly reproducible
frame-for-frame.

---

## 3. Part B — Creative freedom

Everything in this part is behaviour quack has that duck-pond never had. Some
of it is invention; some of it is the visual layer doing work in a 3-D scene
that the Rust did in 2-D and in sound.

### 3.1 Two new activities

**`beg`** (`src/duck.ts:15`) — there are two distinct kinds, and they are
good:

- **The duck that got a bite and wants more.** `stopFeeding` sends any duck
  that ate and finds the pond empty into `beg` at the spot of its last bite
  for 25–40 s (`src/pond.ts:1037-1054`). This is the behaviour the Rust's
  `stop_feeding` *comment* gestures at — *"though ducks on their own hang about
  for a while in case more come"* (`scene.rs:697-698`) — but the Rust never
  implements it. quack does.
- **The curious visitor**, §3.5.

**`fly`** (§4) — a duck on the wing.

### 3.2 The sub-action layer

The Rust's ducks have a position, a velocity and an activity. quack's ducks
have a **third level**: a pose, re-rolled every few seconds, that carries the
body.

`Duck._nextAction()` (`src/duck.ts:875-929`) is a dice table per activity:

| activity | sub-actions and their chances |
| --- | --- |
| `rest` | `sleep` 0.5 (20–70 s) / `stretch` 0.12 (2.6 s) / `look` 0.18 (4–12 s) / `idle` 0.2 (3–10 s) |
| `preen` | `preen` 0.68 (6–18 s) / `stretch` 0.10 / `look` 0.12 / `idle` 0.10 |
| `dabble`, afloat, adult | `upend` 0.26 (3.6–5.8 s, only where the water is over 0.8 m) / `dabble` 0.34 (2–7 s) / `drink` 0.15 (2–4 s) / `idle` 0.25 |
| `dabble`, afloat, duckling | `drink` 0.35 (1.6–3 s) / `look` 0.15 / `idle` 0.5 — "picking insects off the surface" |
| `dabble`, not afloat | `peck` 0.5 (2–5 s) / `look` 0.25 / `idle` 0.25 — grazing the bank, or the ice |
| `swim` / `stray` | `look` 0.2 (3–8 s) / `drink` 0.04 / `idle` 0.76 |
| `feed` | `idle` only (1.5–5 s) — "making for food: there is no time for sightseeing" |
| `beg` | `beg`, with a duration of `1e9` so it never expires on its own |

### 3.3 `upend` — bottom feeding, which the Rust never modelled

The Rust's `Dabble` covers sieving the surface and grazing, and adds a
*splash sound*; it never has a duck stand on its head. quack does, as a
whole-body pivot about the waterline (`src/duck.ts:1009-1039`): a 0.8 s ramp
over, a hold, a 0.75 s ramp back, the feet kicking at 7.5 rad/s, the stern
wagging, and a ring-and-droplet burst at each end. The body tips by
`DUCK.upend.pitch` = 1.62 rad, "a little past vertical" (`src/duck.ts:1368-1372`).

This is a genuine addition, and an ecologically correct one — upending is a
core mallard feeding behaviour, and the Rust's model has no way to express it
because there are no bodies.

### 3.4 The waddle

The whole leg solve (`src/duck.ts:153-168`, `1296-1324`) has no analogue at
all. One hinge per leg at the hip, the stance ramp as an arcsine, the planted
sole holding its spot, the body riding whichever leg is down, the ankle
cancelling the hinge, the lean onto the planted foot and the stern's
answering wag. The cadence falls out of the speed rather than being keyframed,
so a hen at 0.45 m/s waddles at 1.32 cycles/s and a duckling at its shorter
legs waddles faster.

### 3.5 The curious visitor

`src/pond.ts:925-930` — at a bout end, a duck in the water within 46 m of the
stand, with no bread down, may decide to come and look at you:
`CURIOUS.chance` 0.08, at most one at a time, 150–420 s apart. It crosses at a
purpose, then stands about in the shallows in front of you for 25–60 s with
its head up and the odd imploring bill-open — **begging at nothing**.

This is the single most quack-native idea in the port. It is a *reading* of
the human, not a reaction to food, and it gives the viewer a relationship with
the flock that the Rust — which has a listener but no visible one — never
had.

### 3.6 The lap of the lake

`src/pond.ts:331-395` — a duck that would settle to the nearest pastime
sometimes goes off **around** the water instead: `LAP.chance` 0.14 of bout
ends. The circle is drawn fresh through the bird's own position each time, so
the whole bout *is* the round; a random radius (4.5–9.5 m), a random way
round, and two incommensurate harmonics bowing the path in and out by up to a
quarter of the radius. `stepLap` walks the mark 3.5 m ahead along the arc, so
the line the bird swims bows the way the lap does. 0.55–1.5 turns a bout, and
30 % of them swing out of the picture and back.

### 3.7 The sight cone

`src/pond.ts:176-183` — a duck with its head up sees a landing spot within
55 m and inside 1.15 rad of its bill, and sets off for the splash at 25–70 %
of the piece's flight time, so it is *already moving* before the crust lands.
Everyone else only hears it, and comes late.

The Rust can only model the hearing half: peas are *heard*, at
`U(0.3,1.5) + dist/12` seconds (`scene.rs:684`). The sight cone is a
behavioural rule the 2-D original had no way to express, because a 2-D duck
has no bill and no line of sight.

### 3.8 The cursor gaze and the camera

`src/pond.ts:1580-1602` and `src/duck.ts:1202-1211` — every duck pans and
tilts for the pointer, shaped with `tanh` so the middle of the screen is
gentle and the corners reach the full limit (yaw 1.55 rad, pitch 0.72 rad). A
duck that is walking, sleeping, preening, dabbling or pecking ignores the
cursor; only a swimming or loafing, idle/looking/begging duck tracks it. And
when it is making for food, the gaze locks dead ahead — the food overrides the
cursor.

### 3.9 Personal space: hulls and hard separation

`src/pond.ts:1097-1183`. Every duck carries an ellipse hull (0.31 m long,
0.18 m wide, in body-scale units, so a drake is a bigger obstacle than a
duckling) plus a gap: 0.03 m inside a family, 0.10 m between strangers. It is
a nudge first — a soft push from 1.3× the hull distance out, at up to
0.28 m/s — and then `Pond.separate()` runs two passes over every pair,
shares out any overlap and takes the closing speed out of the pair, so the
next frame cannot drive them straight back in. A sleeper or an upended duck
takes a quarter share and is given no new velocity at all.

The Rust has only the first half: adults push apart within 0.6 m
(`scene.rs:1121-1128`), and there is no hard pass, so two ducks in the Rust
can overlap.

### 3.10 The out-of-view rules

`src/pond.ts:161-164, 186-190, 921-924` — a duck that ends a bout outside the
camera's wedge is sent back to its band's water with p = 0.8, and a pastime
already running out there is cut to 50 s. `dabble` is offered only in water
inside the wedge, which was the leak that let a dabbler drift over the line
and then hold position out there for a whole bout.

This is a **camera rule masquerading as a behaviour rule**, and it is worth
naming as such: the Rust's pond is a circle around a fixed listener, so the
whole pond is always in view and the question never arises. quack has a
first-person camera that pans and zooms, so it has to decide what "off
screen" means for a bird.

### 3.11 Bread: a different food economy

The Rust's food is a handful of peas: 12–20 at a time, all landing within
±35° at 2–7 m, each eaten in one peck, sinking after 180 s
(`scene.rs:666-678, 897-903`).

quack's is one crust at a time, thrown by hand, with a much richer lifecycle:

- the throw leaves the hand 1.2 m along the view direction, 1.4 m down
  (`src/main.ts:245-255`), arcs over `clamp(1.0 + 0.06·dist, 1.2, 5.5)` m, and
  lands in `clamp(0.45 + 0.04·dist, 0.6, 1.9)` s;
- the landing is clamped into the chase region — open water, shallows, the
  ice, or the bank — and skewed off the cursor if it lands on top of existing
  bread, re-rolled until it clears every piece by 0.24 m
  (`src/pond.ts:1406-1432`, `src/config.ts:384-389`);
- it takes 2 or 3 bites, shrinking to 80 % each time
  (`src/pond.ts:1076-1088`), and every duck that saw or heard it comes;
- it floats with a little ring every 1.4–4.2 s so it stays findable without
  shouting like the splash it landed with — but a piece on the lid lies still
  and does not ring at all (`src/pond.ts:1552-1563`);
- unclaimed, it shrinks away over 4 s once it is 100 s old
  (`src/pond.ts:1565-1569`).

### 3.12 The mirror rule — ported, and it matters

`src/pond.ts:886-891` keeps the Rust's 0.35 chance for a duck to copy a
leaderless neighbour's activity, sampling only ducks with no leader of their
own, and only for `dabble`, `rest` or `preen`. This is the Rust's
`others_doing` rule (`scene.rs:1063-1066`), verbatim. It is no longer the
*only* mechanism producing the pond's "lively and drowsy spells": the calling
chain (§2.1) is back beside it.

Measured on the Rust: the four leaderless ducks share an activity 11.5 % of
the time against a 4.9 % chance baseline — a 2.3× effect, modest but real
(`duck-pond/behaviors.md` §3.8).

### 3.13 The `startle` pose that never fires

`src/duck.ts:963-966` and `1145-1154` implement a full startle — *"a whirr
that swings from the waterline up over the back, never down through the
surface the duck is sitting on"* — and it is wired into the wing-visibility
test at `src/duck.ts:1260`. But `startAction('startle'` appears **nowhere**.
It is dead code inherited from the bird rig the port came from.

It is still unwired. The new `CallEvent`s give it an obvious trigger later — a
nearby answer could startle a duck once there is an attention model for the
mix — but the stage only exposes levels, not a "this was loud to *this* duck"
signal, so the pose stays dormant rather than firing at random.

---

## 4. Part C — Changed by the flight

The Rust has **no flight at all**: no wings, no `fly` activity, and the only
thing that is ever "airborne" is a pea in the air (`scene.rs:187, 676-677`).
A duck in duck-pond never leaves the water.

quack's flight is a whole subsystem, and it changes behaviour in six ways.

### 4.1 The flight itself

`FLIGHT.chance` 0.18 of bout ends, scaled by `WINTER.bouts` 0.55 in a freeze
(`src/pond.ts:915-916`), become a low pass over the water: 16–34 m long
(`WINTER.flightReach` 5–14 m in winter), 4.2–8.5 s in the air, riding
0.8–1.45 m × body scale over the surface at its peak, with the landing
re-rolled until it is clear of the ducks already there and within 40 m of the
thrower (`src/pond.ts:965-978`, `src/config.ts:310-328`). A bird that has just
come down stays down for 20–45 s, so flights never chain into a shuttle.

### 4.2 A flying duck leaves the flock's economy

`src/pond.ts:1190` skips the flying duck in the steering pass entirely, and
`src/pond.ts:1188` excludes it from the hull, the ripple wake, the cursor gaze
and unclaimed bread. Nothing on the water may nudge it off. It is a bird
above the flock, not of it.

### 4.3 Who is allowed to fly

```ts
// src/pond.ts freeWing()
/** Ducks that follow no one and have no brood fly and roam alone. */
private freeWing(d: Duck): boolean {
    return !d.leader && d.kind !== 'duckling' && this.young(d).length === 0;
}
```

That admits **Clover** and **Bramble** only:

| duck | may fly? | why |
| --- | --- | --- |
| Mabel | no | has a brood |
| Hazel | no | has a brood |
| Clover | **yes** | no leader, no brood |
| Bramble | **yes** | no leader, no brood |
| Rufus | no | has a leader (Clover) |
| the 7 ducklings | no | `kind !== 'duckling'` |

The earlier comment said "nobody to keep station on", which reads as *who
follows you*, though the test is `leader` — who you follow. It now says
"follows no one", which is what the code means. Clover's drake still keeps
station on **her**; she is the one flying.

### 4.4 The Clover–Rufus wrinkle

Because `cue()` copies the leader's activity unless the leader is feeding,
straying or begging (`src/pond.ts:785-786`), and `fly` is not on that list,
Clover taking off **cues Rufus into `fly`** — an activity he can never
perform, because `freeWing` excludes him.

He recovers on the next frame: `case 'fly': if (!d.flying) this.nextActivity(d)`
(`src/pond.ts:706-709`) re-picks him a fresh activity. So the visible effect is
that Rufus's activity label reads `fly` for a frame or two, and — more
substantively — **Rufus re-rolls his whole decision the moment his mate takes
off and again when she lands** (`landFlight` calls `nextActivity`,
`src/pond.ts:469`). A mated drake therefore re-plans his day on his mate's
flight schedule rather than on his own.

**Fixed.** `cue()` now treats `fly` like `feed`, `stray` and `beg`: a follower
whose leader takes off is mapped to `swim`, so Rufus never wears an activity he
cannot perform. The alternative — making `freeWing` test "does anything follow
me" — was rejected because it would ground Clover, the one bird whose
independent flight is part of the pond's character.

Rufus still keeps station on Clover while she is away and picks up her
post-flight activity when she lands. That is the follower rule doing its job,
not an impossible `fly` re-roll.

### 4.5 Flying for bread

`src/pond.ts:536-560` — a free duck that sees a piece of bread 15 m or more
away may take the wing for it, the chance rising linearly to 0.55 at 42 m. It
comes down in the water 1.4–2.6 m off the piece on a random bearing and
paddles the last of the way, so it never lands on its own mouthful, and only
one bird flies for a piece — the rest swim, which is what keeps a far throw
from emptying the pond into the air.

### 4.6 The flight changes the picture rules

A duck on the wing is not in the water, so it is not "in frame"
(`src/pond.ts:162-164` requires `inPond` and not on the lid), and the lap's
wedge test skips the winter case (`src/pond.ts:352`). The flight is
therefore the one behaviour that can take a bird out of the picture on
purpose — and `FLIGHT.far` = 40 m of the thrower is the constraint that keeps
it readable.

---

## 5. Part D — Changed by the winter

The Rust has **no seasons, no ice, no snow and no year** — a grep for
`season|winter|ice|snow` across `duck-pond/src/` returns nothing but
incidental matches inside `voice` and `f64`. duck-pond is a timeless
breeding-season pond.

quack's year is four keyframe seasons, 7 minutes each, 28 minutes round
(`src/seasons.ts:43-114`, `src/config.ts:119`). The **only** variable that
reaches the behaviour model is `world.ice` (`src/world.ts:495-498`); `snow`,
`wind`, `leaf`, `flower` and the light are consumed by the paint and the
particle systems alone. The winter test is `world.ice > WORLD.ice.season`
(0.3), and autumn's `ice: 0.08` never reaches it.

**So the behaviour model has exactly two regimes: open water, and freeze.**
There is no behavioural difference between spring, summer and autumn.

### 5.1 The twenty seasonal rules

| # | rule | summer | winter | where |
| --- | --- | --- | --- | --- |
| 1 | where the flock lives | the depth bands | **the lead** | `src/pond.ts:237-255` |
| 2 | roam target | anywhere in the ellipse | inside `0.95 × r0` of the lead | `src/pond.ts:280-303` |
| 3 | minimum roam distance from self | 14 m | 5 m | `src/pond.ts:281` |
| 4 | lap radius | 4.5–9.5 m | 3–5.5 m | `src/pond.ts:333-336` |
| 5 | lap / flight chance multiplier | 1 | **0.55** | `src/pond.ts:900` |
| 6 | flight reach | 16–34 m | 5–14 m (a hop over the lead) | `src/pond.ts:969-970` |
| 7 | a bid to dabble | stays a dabble | **half become loafing** | `src/pond.ts:960` |
| 8 | a swim bout bound for the lid | — | 4.5 % of them | `src/pond.ts:238-252` |
| 9 | a spell ashore | 35–90 s | 15–45 s on the lid | `src/pond.ts:155-159` |
| 10 | the water pulls at a bout end | 0.75 | 0.85 | `src/pond.ts:910` |
| 11 | a visit ashore | 0.35, or 0.5 for a hen with young | both halved | `src/pond.ts:997-999` |
| 12 | frozen water | swimmable | **not** swimmable — the duck walks on it | `src/pond.ts:133-135` |
| 13 | the ice | — | a legal place to stand | `src/pond.ts:147` |
| 14 | a duck on the lid | — | out of the picture | `src/pond.ts:162-164` |
| 15 | a dabbler on the lid | dabbles | **grazes** — pecks at the ice | `src/duck.ts:892-897` |
| 16 | upending | allowed in water over 0.8 m | never on the lid | `src/duck.ts:903` |
| 17 | a flight landing | a splash | a puff of snow | `src/pond.ts:463-468` |
| 18 | a nibble | a ripple | dust, and no water stirred | `src/pond.ts:1085-1086` |
| 19 | bread landing | a splash | dust | `src/pond.ts:1531-1536` |
| 20 | bread on the lid | floats, bobs, rings | grounded, still, dry tap | `src/pond.ts:1542-1563` |

### 5.2 The lead

`WORLD.lead` = centre (−3.5, 29), r0 = 8.5 m, r1 = 11.5 m, rim wobbled by
±1.85 m in two integer harmonics (`src/config.ts:180-183`,
`src/world.ts:25-32`, `465-477`). The ice closes on it from every edge; the
lead itself never closes.

In a freeze the depth bands are **abandoned entirely**: `pondPoint` sends the
flock to a disc of radius `0.9 × r0` = 7.65 m around the lead, in water
deeper than 0.8 m and not frozen (`src/pond.ts:240-255`). The bands and the
camera's wedge no longer describe where the flock may be.

### 5.3 The ice is a third medium, not a texture

`src/pond.ts:132-148` — a duck may be in the water deep enough to paddle, in
the shallows (0.05–0.3 m, too shallow to paddle and shallow enough to walk
through), or on the lid of the ice. The **steering is identical in all
three**; only the allowed region and the footing change. The same waddle
solve runs on the ice; the duck simply stands at the water level instead of
on the swell (`src/duck.ts:1351-1362`).

`Pond.stranded()` (`src/pond.ts:1381-1396`) is the safety net: a duck standing
where nothing allows it any more — the freeze closed around it, the thaw took
the lid from under it — is walked out to the nearest legal spot, because
steering cannot free a duck that has nowhere to steer.

### 5.4 The half-dabble rule, and why it is right

```ts
// src/pond.ts:957-960
// a freeze leaves a dabble little to find under the surface: half the
// bids to dabble become loafing, so the flock rests and preens its way
// through the cold instead of poking at the same few inches of water
if (this.world.ice > WORLD.ice.season && next === 'dabble' && this.rng.chance(FLOCK.winterDabbleRest)) next = 'rest';
```

This is the best single line of behaviour in the port. Mallards do not feed
the same way all year — winter foraging is a different, much more
opportunistic business — and this is the one place quack says so.

---

## 6. Part E — the "fly/winter" reckoning

Two things are true at once, and it is worth separating them.

**The Rust is right about its own world.** duck-pond is a breeding-season
pond: three hens, two drakes, two broods, and no time axis. A timeless pond
with a breeding flock is a coherent thing to be. Nothing is wrong with it.

**quack inherited that cast into a world where it stops being right.** quack
has a year. And in a year, the flock that duck-pond models is a flock that
should not exist in January.

### 6.1 What quack's winter gets right

The *behaviour* of quack's winter is better than the Rust's absence of
winter, and in places better than a naive reading of the Rust:

- **Gathering at open water.** The lead is exactly what wintering mallards
  do: they concentrate on the last open water. The Rust has no analogue
  because it has no winter.
- **Walking on ice and snow.** Real, and modelled down to the swell being
  pinned on the lid while the open pool still answers the clock
  (`src/world.ts:453-463`).
- **Feeding less.** The half-dabble rule (§5.4) is ecologically sound.
- **Shorter spells on the ice, and more of a pull back to the water**
  (`SHORE.iceStay` 15–45 s against `SHORE.stay` 35–90 s; `returnFromIce` 0.85
  against `returnToWater` 0.75).
- **Boldness.** The curious visitor begging at nothing is a *wintering*
  behaviour — mallards at urban and park ponds learn to approach people, and
  they are bolder in winter than in the breeding season. quack has this
  behaviour all year, but it belongs to winter.

### 6.2 What quack's winter gets wrong

| # | the problem | where |
| --- | --- | --- |
| 1 | **The flock never changes.** The same twelve birds are on the pond in January and in July. | `src/pond.ts:76` — `buildFlock()` is called once and never again |
| 2 | **Seven ducklings in January.** Pip, Dot, Bean, Fig, Nib, Tuft and Sprout are hatched in spring; by winter they are adults, or gone. The TS has seven ducklings standing on the ice with their mothers. | `src/pond.ts:105-117` |
| 3 | **No migration at all.** A grep for `migrat` across `src/` finds two hits and both are *negations* — `// excursion rather than a migration out to the far water` (`src/pond.ts:989`) and *"not a migration"* (`docs/port-notes.md:266`). | — |
| 4 | **No plumage change.** A drake in January wears eclipse plumage — drab mottled brown, not the green head. The TS's palettes are static constants (`src/duck.ts:250-336`); a drake in January looks exactly like a drake in June. | `src/duck.ts:250-294` |
| 5 | **No departures.** In reality the brood goes south, or is shot, or simply does not make it. Nothing in quack ever leaves except by flying a lap and coming back. | — |
| 6 | **The flock is a breeding flock, not a wintering flock.** Wintering mallards flock loosely and in numbers; a hen with a brood of four is a spring bird. The pair-and-brood structure is a breeding-season structure. | `src/pond.ts:83-118` |

### 6.3 The options, honestly

This is a design decision, not a bug, and there are three defensible answers.

1. **Keep the cast and own it.** The pond is one year in a timeless place: the
   same twelve birds, always. The `docs/port-notes.md:266` line — *"a visit is
   one to three bouts — not instant, and not a migration"* — is already
   half this argument. It should be said more plainly somewhere, because a
   viewer who watches a year will notice that the ducklings never grow up.

2. **Make the year a year.** Ducklings hatch in spring and are fledged by
   autumn; in winter the flock is a different flock. This is the real answer
   ecologically, and it is a large change: the cast has to become dynamic, the
   plumage has to change with the season, and the breeding-brood structure
   has to dissolve into a wintering flock.

3. **Drop the winter, or drop the year.** If the cast is to stay fixed, the
   ice is the thing that makes it visibly wrong, because ice is what says
   "winter" to a viewer. The lead, the lid, the snow — the whole of Part D
   would go, and with it quack's best seasonal behaviour.

Of the three, option 1 is the chosen answer: quack is one year in a timeless
place, with the same twelve birds throughout. That sentence now lives in
`docs/port-notes.md`, where the winter notes can point at it. Options 2 and 3
are both larger than they look.

### 6.4 The smaller fly/winter point

Flight and winter are entangled in one place: **`WINTER.bouts` = 0.55** scales
both `FLIGHT.chance` and `LAP.chance` (`src/pond.ts:900, 916, 931`). The
flock flies and swims about less in the cold, and the reason is the same for
both — there is less to do and less to go for.

That is the right way round, and it is worth noting that the Rust has no such
coupling, because the Rust has no winter and no flight to couple.

---

## 7. Appendix A — the constant map

### Ported with the same value

| Rust | quack | value |
| --- | --- | --- |
| `HULL_SPEED` | `FLOCK.hullSpeed` | 0.71 m/s |
| `DUCKLING_HULL_SPEED` | `FLOCK.ducklingHullSpeed` | 0.43 m/s |
| `DUCKLING_BURST` | `FLOCK.ducklingBurst` | 1.26 m/s |
| `BROOD_PACE` | `FLOCK.broodPace` | 0.3 m/s |
| `LOST` | `FLOCK.lost` | 4.0 m |
| `STRAY_EVERY` | `FLOCK.strayEvery` | 900 s |
| swim bout | `FLOCK.swimBout` | 180 s |
| dabble / rest bout | `FLOCK.bout.long` | 60–240 s |
| preen bout | `FLOCK.bout.preen` | 30–120 s |
| the mirror chance | `FLOCK.mirrorChance` | 0.35 |
| the band re-roll | `FLOCK.bandChange` | 0.3 |
| the arrive threshold | `FLOCK.arrive` | 0.7 m |
| the cue divergences | hard-coded in `cue()` | 0.7 / 0.5 / 0.3 |
| the call intervals | `CALLS.interval` | 0.5–8 s first; 0.2–180 s between calls |
| the answer chain | `CALLS.answer` | mate 0.6, others `0.3·(1 − dist/40)`, within 40 m |
| the transition table | hard-coded in `nextActivity()` | 0.65/0.85, 0.45/0.75, 0.4/0.75, 0.5/0.7 |
| cruise speeds | `src/duck.ts:733` | 0.45 / 0.5 / 0.33 m/s |
| the line behind the mother | `src/pond.ts:859-862` | `−0.8 − 0.55·rank`, lateral ×0.25 |
| the drake's shove | `src/pond.ts:840-844` | +0.6 m along his own bearing |
| the straggler threshold | `src/pond.ts:1193-1196` | 3.5 m, speed 0 |
| the stray bout | `src/pond.ts:869-879` | 15–40 s, 5–9 m from the mother |
| the lost hysteresis | `src/pond.ts:642-648` | 1.5 m, and ×2.5 while feeding |
| the four-step shore slide | `src/pond.ts:1317-1344` | full / x / y / tangential |

### Changed

| Rust | quack | why |
| --- | --- | --- |
| 12–20 peas per throw | one crust per throw, 2–3 bites | a hand, not a machine |
| peas only *heard* | heard **and** seen (§3.7) | a bill and a line of sight |
| the pre-pea activity is lost | **restored** (§2.6) | the port fixed the original |
| one region for the water | three media | the ice |
| no flight | a whole flight system | the 3-D rig |
| no year | four seasons | the year |
| personal space by radius | hulls and gaps | body size matters |
| one RNG stream | a master plus one per duck | per-duck jitter and poses |
| `StandardNormal` for the water | value noise for the terrain | the terrain, not the audio |

### Audio (v2)

| Rust | quack | note |
| --- | --- | --- |
| the synthesis, propagation and spatialization | the same Rust `Stage`, running as wasm in a worker | `src/audio.ts`; §2.1 and `docs/audio.md` |
| the dabbling splash rhythm | `Duck.onDabble` → `quack_audio_dabble` | `src/duck.ts:925-935` |
| the bill clack and swallow | `Pond.onPeck` → `quack_audio_peck`, dry or afloat | bread replaces peas |
| the pea plop and bank tap | `Pond.onBreadLand` → `quack_audio_fixed` | the bread's splash and dust have a voice now |
| the water bed, drips, reverb | the same Rust render, driven by `Stage` | `Stage::ambient` and `Stage::render` |

### Dropped

| Rust | quack | note |
| --- | --- | --- |
| `PERSONAL_SPACE` from the listener | — | the camera replaced the listener's radius |

---

## 8. Appendix B — where to look

| concern | quack | duck-pond |
| --- | --- | --- |
| the activity set | `src/duck.ts:15` | `scene.rs:153-167` |
| the roster | `src/pond.ts:83-118` | `scene.rs:1296-1327` |
| the frame loop | `src/pond.ts:1606-1623` | `scene.rs:787-838` |
| thinking | `src/pond.ts:611-772` | `scene.rs:850-946` |
| the cue rule | `src/pond.ts:775-789` | `scene.rs:1006-1021` |
| station keeping | `src/pond.ts:803-880` | `scene.rs:965-1000` |
| the next activity | `src/pond.ts:884-1025` | `scene.rs:1042-1107` |
| steering | `src/pond.ts:1187-1379` | `scene.rs:1109-1184` |
| separation | `src/pond.ts:1097-1183` | `scene.rs:1121-1128` |
| the calling model | `src/pond.ts` (`tickCalls`, `call`), `src/config.ts` (`CALLS`) | `scene.rs:1189-1292` |
| the poses | `src/duck.ts:875-929` | — |
| the waddle | `src/duck.ts:153-168, 1296-1324` | — |
| flight | `src/pond.ts:427-500` | — |
| the ice and the lead | `src/world.ts:465-498` | — |
| the season keys | `src/seasons.ts:43-114` | — |
| every dial | `src/config.ts` | `scene.rs:313-343` |
| the RNG | `src/utils.ts:56-93` | `main.rs:139-142` |
| the working porting record | `docs/port-notes.md` | `duck-pond/exploration.md` |
