bc16 — Battlecode 2016 "Zombie Invasion": the eighth year module - #10
Merged
Conversation
The eighth year module and the first pre-2020 year, ported from
battlecode/battlecode-server-2016 at 11a0b09f26a70da19f33a61ebec4ceaf6e161aa3
with a file:line citation on every rule.
The official 2016 spec is lost (dead S3, dead battlecode.org, no Wayback
copy) and this year's GameConstants has no SPEC_VERSION field at all, so the
ENGINE SOURCE IS THE SPEC: GameWorld.java (1051 lines), InternalRobot.java
(473), RobotControllerImpl.java (886), ZombieControlProvider.java (398),
GameMap.java (874), RobotType.java (390), GameConstants.java (162),
MapLocation/Direction/ZombieSpawnSchedule/ZombieCount/Team/IDGenerator/
GameMapIO were read whole and are cited in the module's own doc comments.
The sim, in the note's own file layout:
constants.nim GENERATED by tools/gen_year_constants.py --year bc16 (2016's
GameConstants is an INTERFACE, so its fields carry no
`public static final` and it needed its own regex), the whole
twelve-row RobotType table with all seventeen constructor
fields, and the outbreak ladder read out of the engine's own
switch
units.nim the pure per-unit arithmetic: four Teams, ten Directions with
2016's SOUTHWARD y axis, six symmetries, the eight derived
predicates, the tabled (int) Math.sqrt(r2) for 0..10 000, the
float64 rubble/move/guard/broadcast arithmetic, and
directionTo's 2.414 fan computed in the engine's own doubles
delays.nim the core/weapon pair and its four mutators, plus the two
composite helpers whose OPPOSITE set/add pairing is the
easiest way to break this year; decrementDelays is pinned to
1.0 (V1) and the engine's whole formula is kept beside it for
the table and the tests
health.nim the two INDEPENDENT infection counters, the viper tick's
exactly-2.0 damage, the health cap, and deathConsequence --
the ordered decision a death makes (rubble or a zombie, never
both)
world.nim state, geometry, terrain, the INSERTION-ordered exec list
with by-value removal, sensing in insertion order, the single
changeHealthLevel mutation point with the mid-turn DESTROYED
check, and every action of rule 3.2
zombies.nim the VERBATIM ZombieControlProvider: the den's three-step
turn, spawnAllPossible's ring and its no-break "last
non-zero type" priority, and processZombie's eight-step
ladder with both RNG draw preconditions exact
economy.nim max(0, 2 - 0.01 * robots) per team per round, A then B
signals.nim the 1000-entry FIFO, the 5/20 per-turn counters, and the
broadcast walk over ALL FOUR TEAMS with its two-counter charge
rules.nim the four-step round loop (rounds numbered FROM ZERO), the
four-rung round-2999 ladder on exact float64 differences, the
float32-narrowed points formula and one game
maps.nim the 22-map pool, the loader, the draw and the map cards
knobs.nim the eleven-knob doctrine sheet, no `chassis` key (D1), with
ABSENT-key defaulting counted (the envelope pin, item 2)
chassis/ fourteen files: kit, econ, archon, combat, micro, turret,
dens, neutral, rubble, infect, comms, bulwark, greenhorn and
scenario16
data/maps/bc16/ carries the 22 converted maps, each with its BUILD-TIME
per-den schedule split (D3) and computed symmetry (D4), so the runtime sim
hashes nothing. tools/convert_maps_bc16.py reproduces Java 8's HashMap
iteration order (h ^ (h >>> 16), bucket hash & (n-1), capacity 16, load factor
0.75, resize splitting each bucket in place) and that emulation was verified
against a real java.util.HashMap on all 98 official rosters, 0 mismatches. All
98 maps parse; the two armageddon maps are REFUSED by flag (V4).
data/atlas_bc16.* is cut from battlecode/battlecode-client-2016 (GPL-3.0,
pinned 317e1f3f) by tools/build_sprite_atlas_bc16.py: 49 sprites, all twelve
robot types at ALL FOUR Team palettes, so this is the first year in the repo
whose art draws a NEUTRAL robot as itself.
TheDuck314/battlecode2016 and bshimanuki/battlecode2016 carry NO LICENCE and
were not cloned, not read, not copied, not vendored, not compiled and not
translated; neither contributes a line. `bulwark` is written from the engine's
own mechanics and from the three archetypes the run's idea text names.
GameVersion GV10 -> GV11 with a prepend-only changelog entry, and
ReplayCompatibleGameVersions EXTENDED to GV04..GV11 (never reset): this run
makes no year-neutral behaviour change at all, so no recorded byte anywhere
changes meaning. rng.nim gains ONE optional parameter -- IDGenerator's first
block, which is 0 in 2016 (ids from 1) against the 10 000 floor every later
year uses -- and every existing call site keeps the default.
The year boundary is compiler-enforced: registry, dispatch (Session variant,
every case arm, statsJson16 and the three name tables), sheet, baselines,
decide (the bc16 preamble and observation), broadcast (beatsFor's bc16 arms
and the five bc16 chrome records) and render (the six-step rubble heat ramp
with hard breaks at 50 and 100, and the four-palette unit sprites) all carry
their bc16 arm.
Measured locally (release Nim, not CI): a 3000-round mirror on `river` runs in
2.2 s -- 0.74 ms/round at a peak of 45 and 41 robots -- well inside the note's
2-5 ms/round estimate and the 130 s perf gate.
Adds the `bc16` variant to `coworld_manifest_template.json` beside the seven already there: `num_agents: 2` inside `game_config`, maxRounds 3000, the mixed pool, 3 games a match and the measured budgets. `config_schema.year` gains `"bc16"`; `game.docs.pages` gains a tenth page for `docs/RULES-BC16.md`; the two `player[]` descriptions name the bc16 chassis. `player[]` and the certification block stay on bc26, unchanged. ONE EDIT IS NOT ADDITIVE and is named here so a reviewer looks for it: `config_schema.maxRounds.maximum` goes 2000 -> 3000, a widened bound that accepts everything it accepted before. bc16's tiebreak fires at the end of round 2999, so 2000 would have refused the year's own default. `results.nim` gains `Bc16GameKeys` (44 keys, 10 of them reused from the year-neutral set) and three end reasons -- `archons_destroyed`, `more_archon_health`, `more_parts_net_worth`. `tools/ci/policies.json` gains bc16's four entries (two LLM champions, two scripted fillers), taking the repo-wide file to 32.
`match.nim`'s `collectGameEvents` gains the bc16 arms -- `zombie_wave`,
`outbreak`, `den_destroyed`, `neutral_activated`, `infection`, `turned`,
`tiebreak` and `unit_milestone`, plus year-tested `first_action`, `rout` and
`archon_lost` -- each bounded per game as `docs/REPLAY.md` records.
`winBonusFor` adds bc16 to the 200 set, because bc16's `points` can favour the
loser: a one-unit margin on a rung with large totals is arbitrarily small.
`years/dispatch.nim`'s `currentRound` arm returns `world.currentRound + 1`.
bc16 is the only year whose world counts from -1 and whose first played round
is 0, exactly as the engine's is; `replay.nim`'s year-neutral deriver indexes
the recorded chain with a ROUNDS-PLAYED count, so the arm converts. Every
event, trace line and viewer clock still carries the engine's 0-based number.
AND A REAL DEFECT: `broadcast.nim`'s `zombie_wave` and `outbreak` beat labels
read `count`, `dens` and `multiplier`, none of which are emitted -- the fields
are `total`, `dens_spawning`, `outbreak_level` and `multiplier_permille`. A
missing key in a `JsonNode` `{}` lookup reads back as ZERO, so the label was
non-empty, passed every word-level assertion, and showed the spectator
"WAVE - 0 zombies from 0 dens at x". Both labels now read the emitted fields
and render the per-mille multiplier as `1.1x`.
…walk Three measured adjustments, made so the doctrine knobs have teeth, and each recorded in `docs/RULES-BC16.md` §Divergences item 20 with its measurement: * `dens.nim` gains a standing strike group (`DenStrikeGroup = 4` attackers, `DenGarrison = 4` held back), a sticky den target and a `DenPressureFloor` that commits while the attackers are still alive. `inStrikeGroup` now outranks the defensive floor in `bulwark.nim`'s `objectiveFor`. Without it the floor reclaimed the whole army every round and the den field was never broken: 6 dens killed across the six small maps, against 20 after. * `econ.nim` gains `openingGuardBias` -- `soldier_viper_aggro` takes -20 points of guard share and moves the viper ahead of the third soldier once the stockpile can afford 120 + 30. The bias is DELIBERATELY ONE-SIDED (`turtle` keeps the cog's own number) because a symmetric +/-20 moved the all-defaults game from 3-of-6 to 1-of-6 on the survival gate. Without it `opening` moved nothing measurable: 116 soldiers against 115. * `archon.nim`'s `archon_spread` biases the parts walk by distance from the friendly archon centroid. Without it `huddle` and `split` produced byte-identical games. `rules.nim` also gains the `-d:bc16Idle` define, the Nim twin of the parity oracle's `bc16idle` bot, whose whole body is `while (true) Clock.yield();`. And `chassis/scenario16.nim`'s header is corrected to say what the file is. It claimed a Java twin and three sub-variants (`-d:bc16ScenarioTurn`, `…Annihilate`, `…Tie`). The Java twin is not written, Tier A' does not run, and the three sub-variant defines are read nowhere -- a build with only one of them silently plays `bulwark`, because they do not imply `-d:bc16Scenario`. The base script IS implemented and does run (357 rounds on `river`, 18 605 trace lines); the header now says exactly that, and `docs/PARITY.md` §bc16 names the gap.
Found by the Tier B map probe in `parity-oracle-bc16`, which reads every map's symmetry and per-den zombie split out of the JVM's own `GameMap` and compares it cell by cell against the committed JSON. `tools/convert_maps_bc16.py` emulates `java.util.HashMap` iteration order so that the build-time per-den split of the public zombie schedule matches the engine's. Its emulator appended a new node within a bucket. The engine reaches that map through `Collectors.toMap`, which is implemented with `HashMap.merge` -- and `merge` PREPENDS. All 22 maps are regenerated; five change bytes (`collision`, `frogger`, `quadrants`, `voluted`, `zigzag`).
The `parity-oracle-bc16` half that lives outside `ci.yml`. It fetches the
published `battlecode-2016.0.2.2.jar` (6 563 607 bytes, sha256 c78ef341...),
compiles a driver and two bots against it ON TEMURIN 8, and diffs whole games
against this sim.
RESULT: all eighteen pairs BIT-EXACT, 833 581 trace lines a side, ledger EMPTY.
* `tools/oracle/bc16/jar.lock` -- the URL, the size, the sha256 and the jar's
own `battlecode-version` file. There is no `SPEC_VERSION` in 2016's
`GameConstants`, so those are the only pins available.
* `tools/oracle/bc16/build_oracle.sh` -- verifies all three pins, asserts the
bundled ASM/XStream/`MethodCosts.txt` and the 54 `.xml` map resources, and
compiles the driver and both bots.
* `tools/oracle/bc16/Bc16Trace.java` -- the driver. NEUTRALs are registered to
a `NullControlProvider`, not the zombie one, or `ZombieControlProvider`
deletes every neutral on round 0; it calls `System.exit()`, because the
sandboxed player threads are non-daemon; and it exits 3 if nothing happened,
because under any JDK newer than 8 the 2016 instrumenter throws on every
player class load and the job would otherwise be green while proving nothing.
* `tools/parity_trace_bc16.nim` -- the Nim side.
* `tools/JavaBc16Tables.java` + `data/bc16/tables.json` -- Tier B: the WHOLE
finite arithmetic domain regenerated from the jar's own classes. Both
non-algebraic functions in their entirety (`Math.pow(k/8000,1.5)` for all
8 001 k, the 25 921-pair `directionTo` lattice), the twelve-row RobotType
table, the outbreak ladder, the guard reduction, the rubble clear map, the
parts income curve and all 42 `GameConstants` fields.
* `tools/ci/parity_tiers_bc16.py` -- the comparator, with a 10-case selftest.
* `tools/ci/parity_ledger_bc16.json` -- empty.
TWO MORE DRIVER DEFECTS THE ORACLE FOUND: `ZombieControlProvider.denQueues` is
keyed by ROBOT ID, not `MapLocation` (so a den's successor on the same square
inherits its queue); and `matchEnded()` nulls the provider's `random` field, so
the state must be read through a held reference rather than reflected after the
match. Both fixed here.
AND ONE COMPARATOR PIN: `String.format("%.6f")` rounds HALF-UP while C's
`printf` -- which Nim's `formatFloat` calls -- rounds HALF-TO-EVEN. On
`checkers` at round 219 the two sides held byte-identical rubble arrays and
printed 88935.090413 against 88935.090412. Both 900-term sums are now emitted
as raw IEEE-754 bit patterns on both sides.
`client/replay_broadcast.html` gains the bc16 block beside the other years', following the starter's chrome verbatim: the CSS block hides the 41 sibling ids under `html[data-year="bc16"]` and the 7 bc16 ids everywhere else, and all 13 `.beat-marker` rules are year-scoped, because six of the thirteen beat names are shared with other years. The readouts are `#bc16-archons` (the headline: both factions' archon tally with a health pip each, a green ring for a zombie infection and a violet one for a viper's -- the only readout that can end the match), `#bc16-horde` (THE HORDE CLOCK, the readout no other year has: zombies alive by type, the next scheduled wave with its composition and how many rounds away, the outbreak level and multiplier, dens standing, and the tiebreak countdown -- kept at every width down to 360 px), `#bc16-econ`, `#bc16-units`, `#bc16-doctrines` and `#bc16-siege` (the endcard war panel with the full tiebreak ledger). `relayout()`'s `--statrail` list, both `onText` hooks, the endcard noun row and the 8-way year guard all gain their bc16 entry.
`tools/gen_bc16_fixture_replay.nim` plays a real three-game match (`frogger`/`checkers`/`river`, side-A slots [1,0,1], seed 2016, 1600 rounds, clinched in two) and writes `tests/fixtures/replay-bc16.json`, 49 471 bytes. It carries ALL THIRTEEN beat kinds, so `tests/test_bc16_beats.nim` proves emission, label and CSS together against a committed artefact rather than inventorying a hand-written list -- which is the shape the bc25 run failed review on.
bc16 bumps `GameVersion` to GV11 (GV10 and every earlier value stay in `ReplayCompatibleGameVersions`, so an older replay still loads). The two committed sibling fixtures were recorded at GV10 and are regenerated with their own in-tree generators; `test_bc22_beats`, `test_bc23_beats` and `test_bc23_replay` are unchanged and still green. `tests/test_bc22_replay.nim` asserted the literal string "GV10". A literal cannot survive an authorised version bump, so it is replaced by MORE checks, not fewer: the fixture's version must equal `GameVersion`, must be a real headline version, must be in `ReplayCompatibleGameVersions`, and that list must still be at least 8 long and still contain GV04.
New: `bc16_fixture.nim` (the shared builder), `table_bc16_delay.nim` (the engine's whole delay-decay domain), and 22 `test_bc16_*.nim` shards covering the cooldown pair, the unit table, rubble, infection, the zombie AI, exec order, sensing, the economy, signals, neutral activation, the end ladder, scoring, the maps, the sheet, greenhorn, the baselines, survival, the knobs, the arithmetic, the beats, the replay document and the perf gate. TWO GATES ARE WORTH NAMING. `test_bc16_knobs.nim` proves every one of the eleven knobs has teeth in a NAMED, SIGNED direction over a 72-game sweep, and asserts across the whole sweep that both seats always built >= 10 units and dealt >= 500 damage -- the anti-inert rule, because no setting of any knob may produce a self-starving faction. `test_bc16_survival.nim` commits the MEASURED survival ratio (3 of 6 mirror games not `archons_destroyed`, 20 dens killed, median 2147 rounds) rather than the assumed one, with BOTH measurement tables inline in its header, and it carries a `-d:bc16BrokenChassis` subprocess negative control that must come back RED -- verified at 15 failures. A horde that overwhelms an unbroken den field is authentic 2016, so the gate's anti-degeneracy work is done by substance floors rather than by the ratio alone. Extended: `test_manifest.nim`, `test_viewer.nim`, `test_constants.nim`, `test_sheet.nim` and `test_determinism.nim` -- the last of which asserts bc16's THREE INDEPENDENT `java.util.Random` STREAMS are three separate objects with separate 48-bit states, and that 2016's `IDGenerator` mints from 1 while every other year's still starts above the 10 000 floor. `test_bc16_beats.nim` additionally asserts each beat label against the VALUE in the event that produced it, not just against the words -- the assertion that would have caught the `zombie_wave`/`outbreak` field-name defect.
`.github/workflows/ci.yml` goes to 11 jobs. NEW JOB `parity-oracle-bc16` (Temurin 8, and it asserts that `javac` REJECTS `--release` so the check cannot pass on a mislabelled toolchain): jar fetch and three-pin verify, Tier B part 1 (all 42 `GameConstants` fields cross-checked against the jar's own loaded classes, naming each deliberately-unported constant rather than skipping it silently), Tier B part 2 (the whole finite arithmetic domain, BYTE-DIFFED against `data/bc16/tables.json`), Tier B part 3 (all 22 maps' symmetry and per-den zombie split, read out of the JVM's own `GameMap` -- the step that caught the `HashMap.merge` defect), eighteen whole games, the anti-vacuity check, Tiers A/A"/C, and an evidence artefact. The anti-vacuity floors are MEASURED, not guessed. Neither oracle bot survives the 3000-round cap -- an idle archon line is eaten by the horde -- so the games run 298-1424 rounds and the traces compare the end round, the winner and the domination factor as well. Measured: 36-162 peak robots, 735 peak zombies summed over the eighteen pairs, 18 of 18 saw an infection, and peak bytecode 1 and 294 against limits of 20 000 and 10 000, i.e. 0 % and 2 %, so neither bot ever leaves the `1.0` branch of the engine's `amountToDecrement` and the V1 comparison is defined to the last round. EXISTING GATES gain bc16: the branch trigger; the `BC16_COMMIT`, `BC16_CLIENT_COMMIT` and `BC16_JAR_URL` env; the test job's timeout 130 -> 150; the four generated-file checks (`gen_year_constants --check`, `convert_maps --check`, `--parse-all` expecting `98 official .xml files parsed, 2 refused`, `atlas --check`); an eighth docker-smoke episode (seed 2016004 draws `river`, 900 rounds, per-seat floors on units, damage and parts and across-pair floors on infections, turned, zombie damage, spawned and outbreaks -- every one measured); an EIGHT-replay check; the wasm viewer at `--timeout 120 --soak 15`; and `wasm_replay_smoke` on both the bc16 smoke replay and the committed fixture. TIER A' IS NOT RUN. The design note specified four scenario bots to force the rare end-ladder rungs. The Nim half of the base bot exists and works; the Java half is not written and the three sub-variants exist on neither side, so the job compares Tier A and Tier A" only. `docs/PARITY.md` §bc16 names the exact shape of the gap rather than leaving coverage to be inferred.
* `docs/RULES-BC16.md` -- new. The game, the round loop as the step list, the four-rung ladder, the verbatim unit table, the delay pair, the zombie AI, the eleven knobs, the scoring, a twenty-item §Divergences list and a §Corrections section recording four places the design note's arithmetic was wrong against the JVM (`2.0 - 0.01*137` is 0.6299999999999999 not 0.63; `60*1.1` is 66.0; `145*(1.0/3.0)` is 48.33333333333333; 100 rubble takes 8 clears and 1000 takes 35, not 14 and 55; and `directionTo(self)` is OMNI, not NONE). * `NOTICE` -- the four sections the year needs. bc16 is the FIRST year here whose upstreams are GPL-3.0 rather than AGPL-3.0 and there are two of them, so the file records why the combined work stays AGPL-3.0, names `battlecode-server-2016` @ 11a0b09f and `battlecode-client-2016` @ 317e1f3f by directory and file family, names the CI-only jar, and names `TheDuck314/battlecode2016` and `bshimanuki/battlecode2016` -- both of which carry no licence anywhere -- as not cloned, not read and not vendored. * `docs/PARITY.md` §bc16 -- the oracle, the three defects it found, the Temurin-8 requirement and its three independent defences, the `NullControlProvider` requirement, the tier table, the measured anti-vacuity numbers, and Tier A' NAMED AS NOT IMPLEMENTED with the exact shape of the gap. * `docs/PROTOCOL.md` §bc16 -- the observation, key by key, and the reply. * `docs/REPLAY.md` §bc16 -- the event vocabulary with its per-game bounds, the three name tables, the four kinds bc16 shares with other years, and why the outbreak multiplier travels as an integer per-mille. * `README.md` -- the variant row, the module paths, the docs link and the 2016 licence trail, including that the official 2016 specification is LOST so the engine source is the spec. * `docs/plans/2026-09-09-battlecode-2016-design.md` -- the accepted note.
…t mask Two defects the first CI run found, both in `parity-oracle-bc16`'s and the test job's plumbing rather than in the year module. * `convert_maps_bc16.py --parse-all` prints its per-map table on **stdout** and its summary line on **stderr**, so `--parse-all | tee /tmp/bc16parse.txt` captured the table and lost the one line the next `grep -q` looks for. The step failed while the log showed `98 official .xml files parsed, 2 refused` three lines above the error. Now `2>&1 | tee`, with the reason in a comment. * The trace-upload step is `if: always()`, so it runs after an earlier step failed -- at which point `/tmp/oracle16/traces` does not exist and its `cd` fails the job a SECOND time, burying the real error under a missing directory. It now reports that no traces were produced and exits 0.
The first CI run failed the Tier B byte-diff on exactly one line of a 304 KB file: `pow_1_5`. Nothing else differed. Root cause, and it is not this port's. `InternalRobot.decrementDelays` calls `Math.pow`. The JLS permits `Math.pow` to be up to **1 ulp** from the exact result and requires only semi-monotonicity, so it is not reproducible between JDK builds -- and it is not: the table was generated on Temurin **8u422** and the runner has Temurin **8u452**. `StrictMath.pow` must reproduce fdlibm bit for bit on every conforming JVM, so `tools/JavaBc16Tables.java` now tables `StrictMath.pow` and records that in the document's own new `pow_1_5_source` key. **The byte-diff therefore stays absolute for the whole file** -- and a new BLOCKING step adds the assertion the change would otherwise have lost: the RUNNING JDK's `Math.pow`, the call the engine actually makes, is within one ulp of EVERY tabled value, and the step prints the census. That is strictly more checking than a `Math.pow` byte-diff, not less. Measured over the whole 8 001-value domain, none of it reachable in this coworld because V1 pins the expression to its `pow(0, 1.5) = 0` branch where all three implementations are exact: Temurin 8u422 Math.pow vs StrictMath.pow 7 221 exact, 780 one ulp, 0 worse glibc pow (the port) vs StrictMath.pow 7 220 exact, 781 one ulp, 0 worse glibc pow vs 8u422 Math.pow 7 996 exact, 5 one ulp, 0 worse `tests/table_bc16_delay.nim` asserts the second row (it asserted the third before), `docs/PARITY.md` §bc16 carries the table and the reasoning, and `docs/RULES-BC16.md` V1 records that the sub-1.0 branch could not have been compared bit-exactly even in principle.
bc22's `knobs.nim` exports constants of exactly these names, `battlecode/sheet.nim` re-exports both years' knob modules, and BOTH MODULES ARE CALLED `knobs` -- so `RetreatHpLo` became an ambiguous identifier in any test that pulls in a year fixture as well as `sheet`, and `tests/test_bc22_sheet.nim` stopped compiling. bc22 had the name first and nothing outside `years/bc16/knobs.nim` reads bc16's pair, so the fix is to stop exporting them rather than to rename bc22's or to edit a bc22 test. The comment at the constants says why, so the next year with a `retreat_hp` knob does not rediscover it.
The converter writes `RobotType`'s own SCREAMING names into the committed map JSON (`STANDARDZOMBIE`), and the probe's Python half looked up `standardzombie`. Every schedule row therefore read back as all-zero, every row was dropped as empty, and the step reported `ours None` against a real JVM row on all 110 dens of all 22 maps -- a comparison that could never have passed and never did. Verified locally against the pinned jar under Temurin 8, not only reasoned about: `compared 22 symmetries and 110 per-den schedules against the JVM's own GameMap`, exit 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
bc16— Battlecode 2016 "Zombie Invasion"The eighth year module. A third team neither cog controls: dens on a
published schedule pour zombies at whoever is nearest, the horde gets stronger
every 300 rounds, a bite turns your own robot against you when it dies, and
losing your last archon loses the game on the spot.
Adds the
bc16manifest variant (num_agents: 2), the year module, bothchassis, the viewer block, 24 new test shards and a new
parity-oracle-bc16CI job. Certification stays on bc26. No bc20/bc21/bc22/bc23/bc24/bc25/bc26
module file, map, atlas, test or variant is edited.
The parity result
parity-oracle-bc16fetches the publishedbattlecode-2016.0.2.2.jar(pinnedby size, sha256 and its own
battlecode-versionfile — there is noSPEC_VERSIONin 2016'sGameConstants), compiles a driver and two botsagainst it on Temurin 8, and diffs whole games.
Three real defects it found, none of which any unit test caught:
Collectors.toMapPREPENDS within aHashMapbucket. It is implementedwith
HashMap.merge, notput.tools/convert_maps_bc16.py's iteration-order emulator appended. All 22 maps regenerated; five changed bytes.
ZombieControlProvider.denQueuesis keyed by robot ID, notMapLocation— so a den's successor on the same square inherits its queue.matchEnded()nulls the provider'srandomfield, so the RNG state mustbe read through a held reference.
And one comparator pin:
String.format("%.6f")rounds HALF-UP while C'sprintfrounds HALF-TO-EVEN. Oncheckersat round 219 the two sides heldbyte-identical rubble arrays and printed
88935.090413against88935.090412. Both 900-term sums now travel as raw IEEE-754 bit patterns.The two edits a reviewer should look for
config_schema.maxRounds.maximum2000 → 3000. The one non-additivemanifest edit — a widened bound that accepts everything it accepted before.
bc16's tiebreak fires at the end of round 2999.
GameVersionGV10 → GV11, with GV10 and every earlier value kept inReplayCompatibleGameVersions. The two committed sibling fixtures areregenerated;
tests/test_bc22_replay.nim's hard-coded"GV10"literal isreplaced by MORE checks, not fewer.
Deviations, all recorded in the tree
the Java half is not written and the three sub-variants exist on neither
side.
docs/PARITY.md§bc16 names the exact shape of the gap.6. 2016 combat arithmetic is slow — a GUARD deals 3.0/round to a zombie
against a level-9 BIGZOMBIE's 1500 HP — so a horde that overwhelms an
unbroken den field is authentic 2016. Both measurement tables (healthy and
-d:bc16BrokenChassis) are inline in the test header and indocs/RULES-BC16.md§Divergences item 16, and the negative control stillcomes back RED.
§Divergences item 20.
docs/RULES-BC16.md§Corrections.dispatch.currentRound's bc16 arm reports rounds PLAYED (item 19), becausebc16 is the only year whose world counts from −1.
A viewer defect fixed on the way
broadcast.nim'szombie_waveandoutbreakbeat labels readcount,densand
multiplier— none of which are emitted. A missing key in aJsonNode{}lookup reads back as ZERO, so the label was non-empty, passed everyword-level assertion, and showed the spectator "WAVE — 0 zombies from 0
dens".
tests/test_bc16_beats.nimnow asserts each label against the VALUE inthe event that produced it.