Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/battlecode/broadcast.nim
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ proc beatsFor*(doc: ReplayDoc, frameOfGameRound: proc (g, r: int): int): JsonNod
let isBc16 = doc.year == "bc16"
## The bc23-only kinds below (`anchor_built`, `island_captured`,
## `island_lost`, `conquest_progress`, `well_transformed`, `well_upgraded`,
## `first_elixir_unit`, `boost_field`, `destabilize_hit`, `duel`) need no
## discriminator, because no other year emits them.
## `first_elixir_unit`, `boost_field`, `destabilize_hit`) need no
## discriminator, because no other year emits them. `duel` is NOT one of
## them — bc22, bc23 and bc16 all emit it — so its LABEL tests the year.
for e in doc.events:
## Pre-match events carry `ms` and `game = -1`, not a round. The two
## doctrine kinds still have a beat (every year's stylesheet ships
Expand Down Expand Up @@ -351,9 +352,16 @@ proc beatsFor*(doc: ReplayDoc, frameOfGameRound: proc (g, r: int): int): JsonNod
" for " & $e.fields{"damage"}.getInt() & ", game " &
$(e.game + 1) & ", round " & $e.round
of "duel":
## bc22 spells `duel` with the SAME field name and a different meaning —
## attackers lost, not launchers — so the label switch tests the year.
if isBc22:
## THREE years emit `duel`, with the SAME field name and TWO meanings.
## bc22 (`years/bc22/rules.nim:275`) and bc16
## (`years/bc16/rules.nim:321`) both count `attackersLostThisRound` —
## every unit that can attack, lost by both sides in the same round —
## and read as a TRADE. bc23 (`years/bc23/rules.nim:397`) counts
## `launchersLostThisRound`, one unit type, and reads as a LAUNCHER
## DUEL. bc16 has no launcher, so it takes bc22's wording; testing
## `isBc22` alone dropped bc16 into bc23's branch and told a bc16
## spectator about a unit its year does not have.
if isBc22 or isBc16:
label = "TRADE — " & $e.fields{"lost"}[0].getInt() &
" attackers lost to " & $e.fields{"lost"}[1].getInt() & ", game " &
$(e.game + 1) & ", round " & $e.round
Expand Down
110 changes: 106 additions & 4 deletions tests/test_bc16_beats.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
##
## Four beat NAMES collide with other years and each needs the year test:
## `first_action` and `rout` (bc22/bc23/bc25 map them the same way and bc16
## joins them), `duel` (bc22's and bc23's, whose field means launchers lost
## rather than attackers lost), and **`archon_lost`**, which bc22 emits with
## `gold_dropped` where bc16 carries `cause` — so the LABEL switch tests the
## year there too.
## joins them), **`duel`** (bc22 and bc16 count ATTACKERS lost, bc23 counts
## LAUNCHERS, all under the same field name), and **`archon_lost`**, which
## bc22 emits with `gold_dropped` where bc16 carries `cause` — so the LABEL
## switch tests the year in both places.
##
## Section 2b is the generalisation of that: no bc16 label may carry ANY
## other year's vocabulary, whether or not the beat name is shared.

import std/[algorithm, json, os, sequtils, strutils, unicode]
import harness
Expand Down Expand Up @@ -212,6 +215,105 @@ block:
check("the ARCHON label carries how many are left",
$e.fields{"archons_left"}.getInt() & " left" in label)

# --- 2b. NO OTHER YEAR'S VOCABULARY -----------------------------------------
block:
## r3-D1: A SHARED BEAT MAY NOT SPEAK ANOTHER YEAR'S VOCABULARY. THREE years
## emit `duel`, with the same field name and two meanings: bc22
## (`years/bc22/rules.nim:275`) and bc16 (`years/bc16/rules.nim:321`) count
## `attackersLostThisRound`, bc23 (`years/bc23/rules.nim:397`) counts
## `launchersLostThisRound`. `beatsFor`'s label switch tested `isBc22`
## ALONE, so bc16 fell through to bc23's branch and a live bc16 killfeed
## read "LAUNCHER DUEL — 1 lost to 1, game 3, round 828" — eleven times in
## one replay, naming a unit the 2016 rule set does not have. Nothing here
## asserted the WORDING of a shared beat PER YEAR, which is why it shipped
## green; this is the same guard `tests/test_viewer.nim` (r2-E1) puts on the
## endcard's shared win-condition branches.
proc firstEventOf(kind: string): MatchEvent =
for e in doc.events:
if e.kind == kind: return e
raise newException(ValueError, "no " & kind & " event in the fixture")
proc duelLabelUnder(year: string): string =
var d = doc
d.year = year
for b in beatsFor(d, frameOf):
if b["k"].getStr() == "duel": return b["label"].getStr()
""
let duel = firstEventOf("duel")
let lostA = $duel.fields{"lost"}[0].getInt()
let lostB = $duel.fields{"lost"}[1].getInt()
let tail = ", game " & $(duel.game + 1) & ", round " & $duel.round
checkEq("bc16's `duel` reads as a TRADE in attackers, which is what bc16 " &
"counts", duelLabelUnder("bc16"),
"TRADE — " & lostA & " attackers lost to " & lostB & tail)
check("and says nothing about a LAUNCHER, a unit bc16 does not have",
"LAUNCHER" notin duelLabelUnder("bc16").toUpperAscii())
## The other two emitters, rendered from the SAME event: the fix widened
## bc16 into bc22's branch and left both of their renderings untouched.
checkEq("bc22's `duel` is unchanged", duelLabelUnder("bc22"),
"TRADE — " & lostA & " attackers lost to " & lostB & tail)
checkEq("and bc23's is unchanged — bc23 really does count launchers",
duelLabelUnder("bc23"),
"LAUNCHER DUEL — " & lostA & " lost to " & lostB & tail)

## AND THE WHOLE FEED, not just the beat that was caught. Every kind bc16
## emits, rendered under bc16, must be free of every word that belongs to
## exactly ONE OTHER year's rule set. bc16's own vocabulary (archon, parts,
## rubble, zombie, den, horde, viper, guard, scout, soldier, turret,
## infection, outbreak) is not on the list, and a word two years share is
## not a leak.
const Foreign = ["launcher", "singularity", "rat king", "cheese", "cats",
"soup", "dirt", "influence", "enlightenment", "crumb",
"duck", "chip", "paint", "adamantium", "mana", "elixir",
"anchor", "boost", "destabilis", "destabiliz", "hq",
"headquarters", "tower"]
## The committed fixture carries every bc16 event kind except the two it
## cannot reach — `tiebreak` fires only when the round limit decides a game
## and `game_abandoned` only on the wall clock — so those two are appended
## here with the fields `match.nim:507-525` and `match.nim:584` give them,
## and the audit really does cover every kind bc16 can put in a killfeed.
var full = doc
full.events.add(ev("tiebreak", game = 0, round = doc.games[0].rounds,
fields = %*{"rung": "more_archon_health", "archons": [2, 2],
"archon_health_tenths": [4210, 3990],
"parts_worth": [1200, 1180]}))
full.events.add(ev("game_abandoned", game = doc.games.len - 1,
round = doc.games[^1].rounds,
fields = %*{"map": doc.games[^1].map}))
let fullBeats = beatsFor(full, frameOf)
var sawTiebreak = false
var sawAbandoned = false
var auditedKinds: seq[string]
for b in fullBeats:
let k = b["k"].getStr()
if k notin auditedKinds: auditedKinds.add(k)
let label = b["label"].getStr()
if "decides it: archons" in label: sawTiebreak = true
if "abandoned at the wall clock" in label: sawAbandoned = true
## A map's NAME is data, not wording: bc16's own pool is what it is, and
## a bc16 map called `towers` would be bc16's own word for it.
var text = label.toLowerAscii()
for g in doc.games: text = text.replace(g.map.toLowerAscii(), "")
for word in Foreign:
check("the bc16 `" & k & "` label says nothing about `" & word & "`: " &
label, word notin text)
check("the appended `tiebreak` really rendered, so its audit is not vacuous",
sawTiebreak)
check("and so did the appended `game_abandoned`", sawAbandoned)
for k in Bc16BeatKinds:
check("and the audit covered the `" & k & "` kind", k in auditedKinds)
## The negative control for the search itself: the SAME events read as bc23
## do trip it, so a clean bc16 audit means the words are absent rather than
## the mechanism being broken. (This is the label bc16 shipped.)
var trippedOn: seq[string]
var d23 = full
d23.year = "bc23"
for b in beatsFor(d23, frameOf):
for word in Foreign:
if word in b["label"].getStr().toLowerAscii() and word notin trippedOn:
trippedOn.add(word)
check("the word search really fires: the same feed read as bc23 trips on " &
trippedOn.join(", "), "launcher" in trippedOn)

# --- 3. STYLE ---------------------------------------------------------------
block:
## A `.beat-marker.<kind>` rule for EVERY KIND THE FIXTURE ACTUALLY
Expand Down
Loading