Skip to content

fix(broadcast): bc16's duel beat is a TRADE, not a LAUNCHER DUEL - #13

Merged
daveey merged 2 commits into
mainfrom
bc16-duel-label
Sep 9, 2026
Merged

fix(broadcast): bc16's duel beat is a TRADE, not a LAUNCHER DUEL#13
daveey merged 2 commits into
mainfrom
bc16-duel-label

Conversation

@daveey

@daveey daveey commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The defect

beatsFor's duel label switch (src/battlecode/broadcast.nim:353) tested
isBc22 alone, so bc16 fell through to bc23's branch and every bc16
killfeed rendered LAUNCHER DUEL — 1 lost to 1, game 3, round 828. bc16 has
no launcher unit.
It happens 22 times in the committed bc16 fixture alone.

Three years emit the beat, with the same field name and two meanings:

emitter counts reads as
years/bc22/rules.nim:275 attackersLostThisRound TRADE
years/bc16/rules.nim:321 attackersLostThisRound TRADE (was LAUNCHER DUEL)
years/bc23/rules.nim:397 launchersLostThisRound LAUNCHER DUEL

bc20/bc21/bc24/bc25 never emit it.

The fix

if isBc22 or isBc16:isBc16 already existed in the same proc and is used
the same way by the archon_lost switch three cases down. bc22's and bc23's
renderings are byte-for-byte unchanged; the same event rendered under each
year:

bc16  TRADE — 1 attackers lost to 1, game 1, round 232   (was LAUNCHER DUEL — 1 lost to 1, …)
bc22  TRADE — 1 attackers lost to 1, game 1, round 232   (unchanged)
bc23  LAUNCHER DUEL — 1 lost to 1, game 1, round 232     (unchanged)

The header comment listed duel among the "bc23-only kinds … no other year
emits them" — the belief that produced the defect — and now states the rule.

Closing the class

tests/test_bc16_beats.nim §2b pins the three renderings exactly and then
audits every beat bc16 emits — all 13 kinds, the fixture's 258 beats plus a
synthesised tiebreak and game_abandoned (the only two kinds the fixture
cannot reach) — against words belonging to exactly one other year's rule set.
Same mechanism as tests/test_viewer.nim's r2-E1 guard from #12.

Counterfactual: reverting if isBc22 or isBc16: to if isBc22: gives
test_bc16_beats: 24 of 6045 checks failed. With the fix, ok (6045 checks).

The audit found nothing else: archon, wave, outbreak, den, activate,
infect, turned, rout, build, game, end and doctrine all pass as they stand.

No year's rules, no oracle, no viewer bundle, no manifest touched. Two files.

…her duel

`beatsFor`'s `duel` label switch tested `isBc22` alone, so bc16 fell through
to bc23's branch and every bc16 killfeed rendered
`LAUNCHER DUEL — 1 lost to 1, game 1, round 232` — eleven times in the
round-17 replay, naming a unit the 2016 rule set does not have.

Three years emit the beat and only two meanings are in play:

  years/bc22/rules.nim:275  w.beat(BeatDuel, "duel", w.attackersLostThisRound[0], …)
  years/bc16/rules.nim:321  w.beat(BeatDuel, "duel", w.attackersLostThisRound[0], …)
  years/bc23/rules.nim:397  w.beat(BeatDuel, "duel", w.launchersLostThisRound[0], …)

bc16 counts attackers, exactly as bc22 does (`years/bc16/world.nim:642`,
`years/bc22/world.nim:512`), so it takes bc22's wording. The switch now names
both attacker-counting years; bc23's branch is untouched and renders
byte-for-byte as before, as does bc22's:

  bc16  TRADE — 1 attackers lost to 1, game 1, round 232   (was LAUNCHER DUEL — 1 lost to 1, …)
  bc22  TRADE — 1 attackers lost to 1, game 1, round 232   (unchanged)
  bc23  LAUNCHER DUEL — 1 lost to 1, game 1, round 232     (unchanged)

The header comment listed `duel` among the "bc23-only kinds … no other year
emits them" — the belief that produced the defect — and now states the real
rule: which years emit it, what each counts, and which wording each gets.
Closes the class the `duel` label was one instance of. `tests/test_bc16_beats.nim`
walked ["build", "rout", "duel", "archon"] asserting only that the kinds were
EMITTED; nothing asserted the WORDING of a shared beat per year, which is why
`LAUNCHER DUEL` shipped green in a bc16 killfeed.

Section 2b now:

  * pins bc16's `duel` to the exact TRADE string built from the event's own
    `lost` pair, and asserts the label contains no "LAUNCHER";
  * renders the SAME event under bc22 and bc23 and pins both of those strings
    exactly, so the fix cannot be reached by changing either of theirs;
  * audits EVERY beat bc16 emits — all 13 kinds, 258 beats of the committed
    fixture plus a synthesised `tiebreak` and `game_abandoned`, the only two
    kinds the fixture cannot reach — against a list of words that belong to
    exactly one other year's rule set (launcher, Singularity, rat king,
    cheese, cats, soup, dirt, influence, Enlightenment, crumb, duck, chip,
    paint, adamantium, mana, elixir, anchor, boost, destabilis/destabiliz,
    HQ, headquarters, tower). A map's own name is stripped first: it is data,
    not wording.

The audit is clean on every other bc16 beat — archon, wave, outbreak, den,
activate, infect, turned, rout, build, game, end, doctrine all pass as they
stand, and no second leak was found. Two guards keep it from being vacuous:
the synthesised beats must actually render, and the same feed read as bc23
must trip the search (it does, on `launcher` — that is the string bc16 shipped).

Counterfactual: with `if isBc22 or isBc16:` reverted to `if isBc22:`,
`nim r --path:src --path:tests tests/test_bc16_beats.nim` reports
"test_bc16_beats: 24 of 6045 checks failed" — the two explicit duel checks and
all 22 duel beats in the fixture. With the fix: "ok (6045 checks)".

No existing assertion was weakened; check count 91 -> 6045, all additions.
@daveey
daveey merged commit 6e89d0f into main Sep 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant