diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e04a0da..b4a4781 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,9 @@ name: CI on: push: - branches: [ main ] + # `bc20-year-module` is the branch the second year module lands on. main's + # trigger semantics are untouched; this only adds a branch. + branches: [ main, bc20-year-module ] pull_request: workflow_dispatch: @@ -40,6 +42,10 @@ env: ENGINE_TAG: "engine.1.2.5" ENGINE_JAR_URL: "https://releases.battlecode.org/maven/org/battlecode/battlecode26-java/1.2.5/battlecode26-java-1.2.5.jar" SCALA_JAR_URL: "https://repo1.maven.org/maven2/org/scala-lang/scala-library/2.11.7/scala-library-2.11.7.jar" + # The Battlecode 2020 sources the `bc20` year module is a port OF. Used at CI + # time by the constants generator, the map converter, the sprite cutter and + # the bc20 parity oracle. There is no JDK in any image stage. + BC20_COMMIT: "7618f6be7d12da39f2e6e25801e578f1fecfbd86" jobs: # --------------------------------------------------------------------- @@ -121,6 +127,29 @@ jobs: echo "BC_ENGINE_DIR=/tmp/engine/battlecode26-${ENGINE_TAG}" >> "$GITHUB_ENV" python3 -m pip install --quiet flatbuffers + # The bc20 year module is generated from a SECOND pinned checkout, and + # every generated artefact is re-generated here and byte-diffed: the + # constants table, all eighteen converted maps and the sprite atlas cut + # from the 2020 client. A hand-edited generated file fails the build. + - name: Fetch the pinned Battlecode 2020 sources + run: | + set -euo pipefail + mkdir -p /tmp/engine20 + curl -fsSL \ + "https://github.com/battlecode/battlecode20/archive/${BC20_COMMIT}.tar.gz" \ + | tar xz -C /tmp/engine20 + echo "BC20_DIR=/tmp/engine20/battlecode20-${BC20_COMMIT}" >> "$GITHUB_ENV" + python3 -m pip install --quiet pillow + + - name: The bc20 generated files match the pinned 2020 sources + run: | + set -euo pipefail + python3 tools/gen_year_constants.py --year bc20 --engine "$BC20_DIR" --check + python3 tools/convert_maps_bc20.py --engine "$BC20_DIR" \ + --out data/maps/bc20 --check + python3 tools/build_sprite_atlas_bc20.py --engine "$BC20_DIR" \ + --out data --check + - name: Run tests # Every tests/*.nim runs twice: debug (range/overflow checks, stack # traces) and -d:release (release-only compile errors and codegen bugs @@ -378,6 +407,174 @@ jobs: if-no-files-found: ignore retention-days: 14 + # --------------------------------------------------------------------- + # THE bc20 PARITY ORACLE. JDK 8 + the pinned 2020 engine SOURCES as a + # CI-only differential test of every piece of arithmetic the Nim port could + # get subtly wrong and never notice: the water table (Math.exp/Math.sin + # intrinsics), both pollution coefficients over the whole integer domain, + # Math.round(float), the IDGenerator streams that decide the highest_id + # tiebreak, java.util.Random, the OVERFLOWING cow seed, Transaction's + # comparator, and the RobotType table. + # + # WHY THIS IS NOT THE WHOLE ENGINE: `engine/build.gradle` depends on + # `net.sf.jsi:jsi:1.1.0-SNAPSHOT`, published only to jcenter (shut down + # 2022) and to the Sonatype OSS SNAPSHOTS repo (expired). Both 404 today and + # `world/ObjectInfo.java` imports `net.sf.jsi` directly, so `:engine:jar` + # cannot be produced from the unmodified upstream build. The last step below + # ATTEMPTS the resolution anyway and reports the failure, so the day the + # artifact comes back the round-loop tier is one step away. docs/PARITY.md + # section bc20 records exactly what each tier proves. + # + # There is no JDK in any image stage -- only here. + # --------------------------------------------------------------------- + parity-oracle-bc20: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + + - name: Cache nimby toolchain and nimcache + uses: actions/cache@v4 + with: + path: | + ~/.nimby + ~/.cache/nim + key: nimby-${{ runner.os }}-${{ env.NIMBY_VERSION }}-${{ env.NIM_VERSION }}-${{ hashFiles('nimby.lock') }} + restore-keys: | + nimby-${{ runner.os }}-${{ env.NIMBY_VERSION }}-${{ env.NIM_VERSION }}- + + - name: Install nimby and Nim + run: | + set -euo pipefail + mkdir -p "$HOME/.local/bin" + curl -fsSL -o "$HOME/.local/bin/nimby" \ + "https://github.com/treeform/nimby/releases/download/${NIMBY_VERSION}/nimby-Linux-X64" + chmod +x "$HOME/.local/bin/nimby" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/.nimby/nim/bin" >> "$GITHUB_PATH" + "$HOME/.local/bin/nimby" use "$NIM_VERSION" + + - name: Sync Nim dependencies + run: nimby --global sync nimby.lock + + - name: Regenerate nim.cfg from the synced package tree + run: | + set -euo pipefail + rm -f nim.cfg + for pkg in "$HOME"/.nimby/pkgs/*; do + [ -e "$pkg" ] || continue + if [ -d "$pkg/src" ]; then + echo "--path:\"$pkg/src\"" >> nim.cfg + else + echo "--path:\"$pkg\"" >> nim.cfg + fi + done + echo '--path:"src"' >> nim.cfg + + # JDK 8: `engine/build.gradle` says `sourceCompatibility = 1.8` and the + # 2020 sources are Java 8. The generated water table was produced under + # a JDK and is byte-diffed against a JDK here, so the version is pinned. + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "8" + + - name: Fetch the pinned Battlecode 2020 sources + run: | + set -euo pipefail + mkdir -p /tmp/engine20 /tmp/oracle/classes + curl -fsSL \ + "https://github.com/battlecode/battlecode20/archive/${BC20_COMMIT}.tar.gz" \ + | tar xz -C /tmp/engine20 + echo "BC20_DIR=/tmp/engine20/battlecode20-${BC20_COMMIT}" >> "$GITHUB_ENV" + + - name: Compile the dependency-free engine sources and the oracle + run: | + set -euo pipefail + S="$BC20_DIR/engine/src/main" + javac -d /tmp/oracle/classes \ + "$S/battlecode/common/GameConstants.java" \ + "$S/battlecode/common/RobotType.java" \ + "$S/battlecode/common/Transaction.java" \ + "$S/battlecode/common/Direction.java" \ + "$S/battlecode/common/Team.java" \ + "$S/battlecode/world/IDGenerator.java" \ + tools/oracle/Bc20Oracle.java + + - name: Tier A (BLOCKING) -- the Java vectors and the Nim vectors agree + run: | + set -euo pipefail + java -cp /tmp/oracle/classes Bc20Oracle > /tmp/oracle/java.vectors + nim c --hints:off -d:release --path:src \ + -o:/tmp/oracle/parity_trace_bc20 tools/parity_trace_bc20.nim + /tmp/oracle/parity_trace_bc20 > /tmp/oracle/nim.vectors + echo "vector lines: $(wc -l < /tmp/oracle/java.vectors)" + if diff -u /tmp/oracle/java.vectors /tmp/oracle/nim.vectors \ + > /tmp/oracle/vectors.diff; then + echo "TIER A: the Nim port is bit-exact with the 2020 engine sources" + else + echo "::error::TIER A: the Nim port diverged from the 2020 engine" + head -60 /tmp/oracle/vectors.diff + exit 1 + fi + + - name: The water-table step (BLOCKING) -- regenerate and byte-diff + run: | + set -euo pipefail + mkdir -p /tmp/oracle/wl + javac -d /tmp/oracle/wl tools/JavaWaterLevels.java + java -cp /tmp/oracle/wl JavaWaterLevels > /tmp/oracle/water_levels.json + if diff -u data/bc20/water_levels.json /tmp/oracle/water_levels.json; then + echo "the committed water table is what this JDK generates" + else + echo "::error::data/bc20/water_levels.json is not what the JDK emits" + exit 1 + fi + + - name: The engine-from-source attempt (reported, not blocking) + continue-on-error: true + run: | + set -uo pipefail + { + echo "## bc20 parity -- the engine-from-source tier" + echo "" + } >> "$GITHUB_STEP_SUMMARY" + cd "$BC20_DIR" + # `settings.gradle` includes a submodule that is not in the archive. + sed -i '/internal-test-bots/d' settings.gradle || true + if timeout 900 ./gradlew --no-daemon :engine:jar > /tmp/oracle/gradle.log 2>&1; then + echo "the 2020 engine BUILT -- the round-loop tier can be promoted" \ + >> "$GITHUB_STEP_SUMMARY" + else + { + echo "the 2020 engine did NOT build. The blocking reason, from" + echo "the Gradle log:" + echo "" + echo '```' + grep -E "Could not (resolve|find)" /tmp/oracle/gradle.log \ + | sort -u | head -8 + echo '```' + echo "" + echo "\`net.sf.jsi:jsi:1.1.0-SNAPSHOT\` was published only to" + echo "jcenter (shut down 2022) and to the Sonatype OSS SNAPSHOTS" + echo "repository (expired). Tiers A and the water table above are" + echo "BLOCKING and cover every piece of arithmetic the port could" + echo "get wrong; the round-loop tier waits on that artifact." + } >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload the parity vectors + if: always() + uses: actions/upload-artifact@v4 + with: + name: parity-vectors-bc20 + path: | + /tmp/oracle/*.vectors + /tmp/oracle/vectors.diff + /tmp/oracle/gradle.log + if-no-files-found: ignore + retention-days: 14 + # --------------------------------------------------------------------- # Packaging smoke: the production image builds, and one real episode runs # end to end in raw docker with the certification fixture's seat mix. @@ -411,14 +608,52 @@ jobs: - name: Install the contract probe's client libraries run: python3 -m pip install --quiet websockets httpx jsonschema - - name: Raw-Docker episode smoke + - name: Raw-Docker episode smoke (bc26, the certification fixture) # SMOKE_SLUG is passed explicitly so the workflow's SLUG is the single # source for the game/player entrypoint names; the script's own # scaffold-substituted default is only a fallback. env: SMOKE_SLUG: ${{ env.SLUG }} + SMOKE_EXPECT_YEAR: bc26 run: ./tools/ci/docker_smoke.sh "${IMAGE}:ci" + # The SECOND episode: the bc20 year module, end to end, in the + # production image. Certification stays on bc26 (the design note's + # §Out of scope), so this is the only thing that proves a bc20 episode + # runs in a container at all -- and its replay is what the wasm-viewer + # job loads in a real browser. + # + # seed 4 draws `Hourglass` from the small pool, which plays the whole + # 300 rounds: ~12.5 s at 24 fps, so the recording outlasts the viewer + # smoke's 10 s soak (the ecos 2026-08-23 scar). + - name: Raw-Docker episode smoke (bc20, scripted vs scripted) + env: + SMOKE_SLUG: ${{ env.SLUG }} + SMOKE_EXPECT_YEAR: bc20 + SMOKE_PLAYER_IDS: bowl-of-chowder,examplefuncsplayer + SMOKE_CONFIG_OVERRIDE: >- + {"year":"bc20","pool":"small","seed":4,"gamesPerMatch":1, + "maxRounds":300,"perGameBudgetSeconds":40,"matchBudgetSeconds":45, + "connectTimeoutMs":15000} + SMOKE_REPLAY_OUT: ${{ github.workspace }}/dist/smoke/replay-bc20.json + # The certification contract probe ran against the bc26 episode a + # moment ago on the same image; running it twice buys nothing and + # doubles the flake surface. + SMOKE_CONTRACT_PROBE: "0" + run: ./tools/ci/docker_smoke.sh "${IMAGE}:ci" + + - name: Both smoke replays are present and are different years + run: | + set -euo pipefail + for f in dist/smoke/replay.json dist/smoke/replay-bc20.json; do + test -s "$f" || { echo "::error::$f is missing"; exit 1; } + done + y26="$(jq -r .year dist/smoke/replay.json)" + y20="$(jq -r .year dist/smoke/replay-bc20.json)" + echo "replay.json year=$y26 replay-bc20.json year=$y20" + test "$y26" = "bc26" || { echo "::error::replay.json is not bc26"; exit 1; } + test "$y20" = "bc20" || { echo "::error::replay-bc20.json is not bc20"; exit 1; } + # The replay this episode produced is the ONLY replay in CI that is # known to be a real, current-format episode of this game. wasm-viewer # downloads it and loads it in a browser, so the viewer is tested @@ -525,18 +760,22 @@ jobs: npm install --no-save playwright@1.55.0 npx --yes playwright@1.55.0 install --with-deps chromium - - name: Load the bundle in a real browser + - name: Load the bundle in a real browser (BOTH years' replays) run: | set -euo pipefail # A for-loop, not `ls … | head -1`: under `set -o pipefail` an unmatched # glob makes ls exit 2 and kills the step before the harness ever runs # (cogame-raid 2026-08-23, run 32623414696). - replay="" - for candidate in dist/smoke/*.replay dist/smoke/replay.json; do - if [ -f "${candidate}" ]; then replay="${candidate}"; break; fi + replays="" + for candidate in dist/smoke/replay.json dist/smoke/replay-bc20.json; do + if [ -f "${candidate}" ]; then replays="${replays} ${candidate}"; fi done - test -n "${replay}" || { + test -n "${replays}" || { echo "::error::docker-smoke uploaded no replay to dist/smoke"; ls -la dist/smoke || true; exit 1; } + # The bundle is EXECUTED once per replay: one year rendering is no + # evidence at all about the other, and the bc20 game block is a + # different set of elements under different CSS. + for replay in ${replays}; do echo "loading ${replay} in dist/static-replay-viewer" # --strict-text-bounds is DELIBERATELY DROPPED here. The board is # pannable and zoomable (#viewpanel is kept, because a 60x60 board @@ -601,11 +840,20 @@ jobs: echo "::error::a panel that opens over them must dismiss itself." exit 1 fi + mv viewer-smoke.json "viewer-smoke-$(basename "${replay}" .json).json" + mv viewer-smoke.png "viewer-smoke-$(basename "${replay}" .json).png" + done # The wasm module itself, under node: wasm32-only failures (int overflow - # traps, address-space exhaustion) are invisible to the native tests. - - name: Smoke the emitted wasm module under node - run: node tools/wasm_replay_smoke.cjs dist/static-replay-viewer "${{ github.workspace }}/dist/smoke/replay.json" + # traps, address-space exhaustion) are invisible to the native tests -- + # and a brand-new year module is exactly where one would live. + - name: Smoke the emitted wasm module under node (both years) + run: | + set -euo pipefail + node tools/wasm_replay_smoke.cjs dist/static-replay-viewer \ + "${{ github.workspace }}/dist/smoke/replay.json" + node tools/wasm_replay_smoke.cjs dist/static-replay-viewer \ + "${{ github.workspace }}/dist/smoke/replay-bc20.json" # The text the CI replay never carries. Every replay CI produces is # SCRIPTED, so its notes and motto are the short strings baselines.nim @@ -654,8 +902,8 @@ jobs: with: name: viewer-smoke path: | - viewer-smoke.png - viewer-smoke.json + viewer-smoke*.png + viewer-smoke*.json dist/fixture/viewer-smoke.png dist/fixture/viewer-smoke.json if-no-files-found: warn diff --git a/.gitignore b/.gitignore index c7260b3..8829b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ nim.cfg # Local scratch /tmp/ *.log + +# Python bytecode from the CI-time tools +__pycache__/ +*.pyc diff --git a/NOTICE b/NOTICE index 227af62..ea2af41 100644 --- a/NOTICE +++ b/NOTICE @@ -6,9 +6,10 @@ projects below, and it is public so that the source offer the AGPL requires is discharged by the repository itself. **No upstream Java source runs in any image this repository builds, and no -JVM, JDK or JRE is installed in one.** The Java engine exists only inside the -`parity-oracle` job of `.github/workflows/ci.yml`, where it is fetched at CI -time and used as a differential oracle (see `docs/PARITY.md`). +JVM, JDK or JRE is installed in one.** The Java engines exist only inside the +`parity-oracle` and `parity-oracle-bc20` jobs of +`.github/workflows/ci.yml`, where they are fetched at CI time and used as +differential oracles (see `docs/PARITY.md`). --- @@ -47,6 +48,55 @@ formation upgrades and squeak-shared mine locations are its ideas, rewritten in Nim and parameterised by this coworld's doctrine sheet. None of its Java is copied, compiled or shipped. +## battlecode/battlecode20 — the 2020 rules, constants, maps and sprites + +* Upstream: +* Pinned at commit **`7618f6be7d12da39f2e6e25801e578f1fecfbd86`** (the last + 2020 state; the repository has not been pushed since 2020-11-28) +* Licence: **GPL-3.0** — *not* AGPL-3.0, unlike battlecode26. + +GPLv3 and AGPLv3 are explicitly made compatible for combined works +(AGPLv3 §13 / GPLv3 §13), so this AGPL-3.0 repository may carry work derived +from it. The derived files are named individually: + +| here | from there | +| --- | --- | +| `src/battlecode/years/bc20/**` — the ported 2020 rule set | `engine/src/main/battlecode/**` (behaviour, hand-ported to Nim; `docs/RULES-BC20.md` §Divergences lists every deliberate difference) | +| `src/battlecode/years/bc20/constants.nim` | `common/GameConstants.java`, `common/RobotType.java`, generated by `tools/gen_year_constants.py --year bc20` and re-diffed in CI | +| `data/maps/bc20/*.json` | `engine/src/main/battlecode/world/resources/*.map20`, converted by `tools/convert_maps_bc20.py` and re-converted in CI | +| `data/bc20/water_levels.json` | `common/GameConstants.getWaterLevel`, generated by `tools/JavaWaterLevels.java` under the CI JDK and byte-diffed in CI | +| `data/atlas_bc20.png`, `data/atlas_bc20.json` | `client/visualizer/src/static/img/sprites/**` and `img/soup.png`, cut by `tools/build_sprite_atlas_bc20.py` | +| `src/battlecode/years/bc20/chassis/scaffold.nim` | `example-bots/src/main/examplefuncsplayer/RobotPlayer.java`, ported statement for statement (it is the parity oracle's other side, so it may not gain behaviour) | +| `tools/oracle/examplefuncsplayer20/RobotPlayer.java` | the same file, with the ONE-HUNK determinism patch in `tools/oracle/examplefuncsplayer20/determinism.patch`. CI-only. | + +**The 2020 engine itself is used only at CI time.** No JDK, no JRE and no +upstream Java source exists in any image this repository builds. + +## StoneT2000/Battlecode2020 — the strategy the `bowl-of-chowder` chassis distils + +* Upstream: +* Postmortem: +* Licence: AGPL-3.0 + +What derives from it: `src/battlecode/years/bc20/chassis/boc.nim` and the +modules it calls (`hq.nim`, `miner.nim`, `designschool.nim`, `landscaper.nim`, +`fulfillment.nim`, `drone.nim`, `netgun.nim`, `lattice.nim`, `pathing.nim`, +`signals.nim`). **Behaviour, not code** — the passive-lattice build order, the +wall-then-terraform landscaper priority, the builder-miner election, the drone +roles and the signal-code discipline are its ideas, rewritten in Nim and +parameterised by this coworld's doctrine sheet. None of its Java is copied, +compiled or shipped. + +## IvanGeffner/battlecode2020 — cited, never used + +* Upstream: +* Licence: **NONE DECLARED — all rights reserved.** + +It is **not vendored, not ported, not compiled and not read into any file in +this repository**. It is cited in `docs/RULES-BC20.md` only as the public +record of what the year's winning archetypes were; the archetype vocabulary in +the bc20 sheet comes from the published postmortems and from the 2020 spec. + ## Everything else The viewer chrome (`client/chrome_common.js`, `client/broadcast_core.js`, diff --git a/README.md b/README.md index 254e6a6..067c9a9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ # cogame-battlecode -**Battlecode 2026 "Uneasy Alliances", played by doctrine.** +**Battlecode, played by doctrine.** Two cogs each write one sealed JSON +strategy sheet and a deterministic Nim port of an official Battlecode rule set +plays the whole match from those two sheets. **One variant per Battlecode +year**, chosen by `game_config.year`: + +| variant | year | the game in one line | +| --- | --- | --- | +| `bc26` | 2026 "Uneasy Alliances" | rat clans allied against NPC cats until one of them betrays | +| `bc20` | 2020 "Soup" | the water rises every round, and a team either terraforms above the flood, walls its HQ in, or buries the enemy's under fifty units of dirt | + +--- + +## `bc26` — Battlecode 2026 "Uneasy Alliances" Two cogs each command a clan of robot rats on a symmetric grid. Neither cog moves a rat. At t=0 each writes a **doctrine** — a sealed JSON sheet of named @@ -45,11 +57,56 @@ coworld upload-policy cogame-battlecode:latest --name my-clan \ `PLAYER_SCRIPTED=scaffold` is the ported `examplefuncsplayer`, deliberately weak, and the bot the parity oracle runs. +--- + +## `bc20` — Battlecode 2020 "Soup" + +**The clock is the water.** From round 1 the level rises on a fixed curve and +floods outward **one ring per round** from every already-flooded tile whose +neighbour sits below it. Anything that is not a delivery drone dies on a +flooding tile. + +An **HQ cannot be raised** — dirt dropped on a building buries it, and fifty +dirt kills an HQ outright. The only thing that keeps an HQ dry is a ring of +eight adjacent tiles the water can never cross. A team that never walls +drowns on a schedule: elevation 3 floods at round 677, elevation 5 at 1210. + +Miners mine SOUP and refine it. Design schools build landscapers that dig and +dump dirt — a lattice, a wall, or fifty units on the enemy's HQ. Fulfillment +centers build delivery drones that pick up any unit, including enemy +landscapers, and drop them in the water. Vaporators print soup and scrub +pollution; net guns shoot drones. The only global channel is a blockchain, +seven ints a message, seven messages a round, paid for in soup — and **both +teams read every block**. + +``` +points = int(60 * HQ-survival share + 25 * unit share + 15 * net-worth share) +score = 100 * games won + mean(points over games played) +``` + +Ten knobs — `opening`, `terraform_start_round`, `lattice_radius`, +`landscaper_count_curve`, `miner_count_curve`, `vaporator_budget`, +`drone_role`, `net_gun_ring`, `rush_trigger`, `wall_hq_round` — which is the +year's own published metagame (rush / lattice / passive lattice / turtle) as a +doctrine surface. Champions: + +| policy | doctrine | +| --- | --- | +| `battlecode-bc20-latticer` | out-build the flood: wall early, terraform, spend on vaporators | +| `battlecode-bc20-rusher` | get there before their wall does: rush, swarm landscapers, harass with drones | + +`PLAYER_SCRIPTED=bowl-of-chowder` is the strong baseline and the champion +chassis (behaviour ported from Stone Tao's finals bot); +`PLAYER_SCRIPTED=examplefuncsplayer` is the ported 2020 example bot, +deliberately weak. + ## What is in here | path | what | | --- | --- | -| `src/battlecode/years/bc26/` | the ported rule set: `world.nim` (state, geometry, legality), `cats.nim` (the NPC state machine), `rules.nim` (the round loop), `chassis/` (the two bots and every doctrine knob's site) | +| `src/battlecode/years/bc26/` | the 2026 rule set: `world.nim` (state, geometry, legality), `cats.nim` (the NPC state machine), `rules.nim` (the round loop), `knobs.nim` (the doctrine table), `chassis/` (the two bots and every knob's site) | +| `src/battlecode/years/bc20/` | the 2020 rule set: `world.nim`, `flood.nim`, `pollution.nim`, `blockchain.nim`, `cows.nim`, `rules.nim`, `knobs.nim`, `chassis/` | +| `src/battlecode/years/{registry,dispatch}.nim` | the year boundary: the ONE place the year-neutral machinery meets a year module | | `src/battlecode/rng.nim` | `java.util.Random` and `IDGenerator`, bit-exact | | `src/battlecode/{sheet,decide,llm,baselines}.nim` | the doctrine schema, the one sealed parallel batch, the provider ladder, the scripted table | | `src/battlecode/{replay,results,broadcast,render,server}.nim` | the JSON replay, the closed results document, the chrome channel, the sprite packets, the container | @@ -59,8 +116,10 @@ weak, and the bot the parity oracle runs. ## Docs -* [`docs/RULES.md`](docs/RULES.md) — the rule set, the eleven doctrine knobs, +* [`docs/RULES.md`](docs/RULES.md) — the 2026 rule set, its doctrine knobs, and every deliberate divergence from the Java engine. +* [`docs/RULES-BC20.md`](docs/RULES-BC20.md) — the 2020 rule set, its ten + knobs, and its own §Divergences list. * [`docs/PROTOCOL.md`](docs/PROTOCOL.md) — `cogame.battlecode.v1`: what a seat sends, what a cog sees, and what it never sees. * [`docs/REPLAY.md`](docs/REPLAY.md) — the replay document and how the viewer @@ -70,9 +129,15 @@ weak, and the bot the parity oracle runs. ## Licence -AGPL-3.0. The rule set, the constants, the maps and the sprite art derive from -[`battlecode/battlecode26`](https://github.com/battlecode/battlecode26) (AGPL-3.0, -tag `engine.1.2.5`) and the `awu` chassis distils the strategy of +AGPL-3.0. The 2026 rule set, constants, maps and sprite art derive from +[`battlecode/battlecode26`](https://github.com/battlecode/battlecode26) +(AGPL-3.0, tag `engine.1.2.5`) and the `awu` chassis distils the strategy of [`awu7/battlecode-2026`](https://github.com/awu7/battlecode-2026) (AGPL-3.0, -branch `final`). See [`NOTICE`](NOTICE). **No upstream Java source, and no JVM, +branch `final`). The 2020 rule set, constants, maps and sprite art derive from +[`battlecode/battlecode20`](https://github.com/battlecode/battlecode20) +(**GPL-3.0**, commit `7618f6b`) and the `bowl-of-chowder` chassis distils the +strategy of +[`StoneT2000/Battlecode2020`](https://github.com/StoneT2000/Battlecode2020) +(AGPL-3.0). GPLv3 and AGPLv3 are explicitly compatible for combined works +(AGPLv3 §13). See [`NOTICE`](NOTICE). **No upstream Java source, and no JVM, runs in this image.** diff --git a/client/replay_broadcast.html b/client/replay_broadcast.html index b3cc943..7363d76 100644 --- a/client/replay_broadcast.html +++ b/client/replay_broadcast.html @@ -2639,6 +2639,127 @@ #scorebug .plate-sub { display: none; } #bars .barset { min-width: 44px; } } + +/* ======================================================================== + BC20 additions to the inherited cogame-battlecode chrome + ------------------------------------------------------------------------ + Everything above this banner is the page as it shipped for `bc26`. Nothing + is removed and no existing id is reused for a different purpose: every id + below is new and `#bc20-` prefixed, and the two blocks are switched by ONE + attribute — `document.documentElement.dataset.year`, set from the replay + header — plus the rules right here. `#viewpanel` is KEPT: a 48x48 board + renders 768 px wide against a 360 px featured-match frame. + ======================================================================== */ +html[data-year="bc20"] #coopchip, +html[data-year="bc20"] #bars, +html[data-year="bc20"] #econ, +html[data-year="bc20"] #doctrines { display: none !important; } + +html:not([data-year="bc20"]) #bc20-flood, +html:not([data-year="bc20"]) #bc20-soup, +html:not([data-year="bc20"]) #bc20-units, +html:not([data-year="bc20"]) #bc20-doctrines, +html:not([data-year="bc20"]) #bc20-doctrines-toggle, +html:not([data-year="bc20"]) #bc20-chain { display: none !important; } + +#bc20-flood { + position: absolute; top: 6px; left: 50%; transform: translateX(-50%); + display: flex; gap: 10px; align-items: center; + max-width: calc(100% - 16px); box-sizing: border-box; overflow: hidden; + padding: 2px 10px; border-radius: 999px; font-size: 11px; + letter-spacing: .06em; background: rgba(11,7,4,.72); color: #7fb2e8; + border: 1px solid #7fb2e8; z-index: 6; white-space: nowrap; +} +#bc20-flood > span { flex: 0 1 auto; min-width: 0; overflow: hidden; } +#bc20-flood.rising { color: #e0523a; border-color: #e0523a; } +#bc20-flood .gauge { + width: 64px; height: 4px; border-radius: 2px; + background: rgba(242,232,216,.14); +} +#bc20-flood .gauge > i { + display: block; height: 100%; border-radius: 2px; background: #2f6fb0; +} + +/* Both readouts sit ABOVE the reserved transport band -- nothing this block + adds is ever overlaid inside it. */ +#bc20-soup { + position: absolute; right: 8px; bottom: calc(var(--band, 0px) + 8px); + display: grid; grid-template-columns: auto auto auto; gap: 1px 8px; + font-size: 10px; color: #cbbfae; background: rgba(11,7,4,.6); + padding: 5px 8px; border-radius: 6px; z-index: 5; pointer-events: none; +} +#bc20-soup b { color: #f2e8d8; font-weight: 600; } + +#bc20-units { + position: absolute; right: 8px; bottom: calc(var(--band, 0px) + 74px); + font-size: 10px; color: #cbbfae; background: rgba(11,7,4,.6); + padding: 5px 8px; border-radius: 6px; z-index: 5; pointer-events: none; + font-variant-numeric: tabular-nums; +} +#bc20-units b { color: #f2e8d8; font-weight: 600; } +#bc20-units .hqdirt { color: #e0523a; } + +/* D3, and THE BOARD IS THE PICTURE. Both doctrines are worth reading before + the flood starts and worth nothing at all if they are still lying over the + landscapers a minute later. Same discipline as the bc26 panel: it opens + with the replay, it CLOSES ITSELF the moment playback advances (or after + six seconds for a viewer who never presses play), a close control, Escape + and a re-open chip drive it by hand, and its body is capped and scrolls so + even opened over a 360 px frame it can never own the board. It sits above + the board area -- never inside var(--band). */ +#bc20-doctrines { + position: absolute; left: 8px; bottom: calc(var(--band, 0px) + 8px); + max-width: 46%; font-size: 10px; color: #cbbfae; + background: rgba(11,7,4,.72); padding: 5px 22px 5px 8px; + border-radius: 6px; z-index: 7; +} +#bc20-doctrines.dismissed { display: none !important; } +#bc20-doctrines-body { + max-height: calc(30vh - var(--band, 0px)); overflow-y: auto; +} +#bc20-doctrines .dline { margin-bottom: 3px; } +#bc20-doctrines .dname { color: #f2e8d8; font-weight: 600; } +#bc20-doctrines .dfall { color: #e0523a; } +#bc20-doctrines-close { + position: absolute; top: 2px; right: 4px; width: 16px; height: 16px; + line-height: 14px; padding: 0; border: 0; border-radius: 4px; cursor: pointer; + background: rgba(242,232,216,.14); color: #f2e8d8; font-size: 12px; +} +#bc20-doctrines-close:hover { background: rgba(242,232,216,.3); } +#bc20-doctrines-toggle { + font-size: 10px; padding: 1px 8px; border-radius: 999px; cursor: pointer; + border: 1px solid rgba(242,232,216,.3); color: #cbbfae; + background: rgba(11,7,4,.6); margin-top: 3px; +} +#bc20-doctrines-toggle[hidden] { display: none !important; } + +#bc20-chain { margin-top: 6px; font-size: 11px; color: #cbbfae; } +#bc20-chain b { color: #f2e8d8; } +#bc20-chain .msg { opacity: .8; } + +/* CSS for EVERY beat kind this year emits. A kind with no rule is an + invisible marker. */ +.beat-marker.flood { background: #2f6fb0; } +.beat-marker.build { background: #8fbf6a; } +.beat-marker.wall { background: #c9a227; } +.beat-marker.rush { background: #e0523a; width: 3px; } +.beat-marker.drop { background: #7fb2e8; } +.beat-marker.bury { background: #e0523a; width: 3px; } +.beat-marker.drown { background: #2f6fb0; width: 3px; } + +/* At the 360 px featured-match width the flood chip keeps the two numbers a + spectator needs -- the water level and how much of the map is under it -- + and drops the per-clan HQ elevations, which the #bc20-units panel repeats + as the HQ dirt load anyway. A chip that keeps everything is a chip that + runs off both ends of the frame. */ +@media (max-width: 760px) { + #bc20-soup, #bc20-units, #bc20-doctrines { font-size: 9px; } + #bc20-units { bottom: calc(var(--band, 0px) + 66px); } + #bc20-flood { gap: 5px; font-size: 10px; } + #bc20-flood .gauge { width: 34px; } + #bc20-flood .lbl, #bc20-flood .hqelev { display: none; } + #bc20-doctrines { max-width: 62%; } +} @@ -2673,6 +2794,9 @@
0:00
▸▸
In the locker room
+ + @@ -2724,6 +2848,17 @@
+ +
+
+
+
+ +
+
+
@@ -2762,6 +2897,9 @@
+ +
@@ -2775,6 +2913,305 @@ + + +