Skip to content

sap2: Tier 3, measured - and the two layout consumers it broke - #12

Open
pjsny wants to merge 2 commits into
mainfrom
sap2-tier3-measured
Open

pjsny wants to merge 2 commits into
mainfrom
sap2-tier3-measured

Conversation

@pjsny

@pjsny pjsny commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Tier 3's ten pets and three foods, every value measured against the shipped build rather than a scrape, ported onto the faint machinery #9 landed. Spider's summon is no longer a documented hole - it has a Tier-3 roster to draw from.

Gate

audit_roster.py --tier 3 reports zero WRONG and zero MISSING rows for tiers 1-3. The 6 WRONG and 85 MISSING it does print are tier 4-6 pool sizes and content, which is the roster cap doing its job.

Check Result
Exact surviving line-ups 300/300 tier 1, 300/300 tier 2, 300/300 tier 3
Tier-3 winners 300 boards, 300 agree, 0 disagree
Holed tier-3 boards 200/200 (195 with a gap to close)
Scripted shop checks 26/26
Shop fuzz at turn 5 no divergence
Shop→battle bridge no divergence
Env tests 84 (72 + 12 Tier-3)

Where the scrape was wrong

PR #10 implements these from data/turtle_pack. Measuring them found, among others:

  • Rabbit buffs the eater, not itself (TargetsTriggerTarget), and its three activations are per turn, not a lifetime cap
  • Birthday Cake exists and is tier-3 rollable - without it every tier-3-and-later food roll draws 1/8 from the wrong set
  • Badger hits the nearest living body each way across the merged line, not "the enemy front when there's no ally ahead" - and the percentage floors (7 attack → 3/7/10 by level), as does Dodo's, which touches attack only
  • Garlic's floor is 2, not 1: 3 damage lands as 2, and 2 lands as 2
  • Salad Bowl is played on the board, not fed to a pet - an aimed PlaySpell is dropped outright, no gold spent
  • Dolphin breaks ties at random, so it consumes the battle RNG stream
  • Ox's Melon grant wakes Rabbit - the trigger bus counts a perk landing on a friendly as a food played on it, where a plain buff does not

Four rules Tier 3 exposed that were already wrong

None of these are Tier-3 rules; Tier 3 is just the first roster that can see them:

  1. Damage eats the temporary health before the permanent half.
  2. A Pilled pet is mid-faint for the whole of its own faint, so its own splash cannot target it.
  3. A lethal hit does not cancel the hurt trigger - what a dying pet loses is eligibility as a target. That replaces the file's old "survival is the whole gate" reading with one rule instead of two.
  4. The summon-push fallback when there is no room behind was wrong; six Sheep layouts pin it.

The two consumers this broke, and the lesson

Tier 3 widened a team slot by two floats and the species one-hot by ten. Neither consumer failed loudly:

  • bots/sap2/greedy is self-contained by design, so its copy of the widths went stale and it read garbage. It did not crash - it just played badly, and the only symptom was its rating against random dropping 1804 → 1521 Elo. Now 1838 vs 1162.
  • tools/visualize_sap2.py derived the species one-hot width by subtracting every other field from the slot width. That over-counted by exactly the two new floats, so argmax ran past the one-hot and returned species 35 - an id no table has.

The derivation was written to be the robust option, and it is the opposite: subtracting known fields from a total silently absorbs any field you did not know about. So NUM_ALL_SPECIES, NUM_SHOP_SPECIES and NUM_FOODS are now exported and consumers read them.

Credit

PR #10's Tier-3 structure was the starting point and its tier assignments were all correct. Its remaining defect list is #11.

Philip Snyder added 2 commits September 17, 2026 22:14
Ten pets (Badger, Camel, Dodo, Dog, Dolphin, Elephant, Giraffe, Ox,
Rabbit, Sheep), three foods (Birthday Cake, Garlic, Salad Bowl), the Ram
token, three perks (Garlic, Melon, Birthday Cake), and Spider's summon -
the one hole Tier 2 shipped with on purpose. SAP2_ROSTER_TIER is 3; what
is left behind the cap is tiers 4-6, not a rule inside a shipped tier.

Every value was read out of the shipped build: sap/spec.py for the
templates, sap/ability_check.py --pet X --levels to FIRE each one, and a
new sap/tier3_drive.py in policy-clash-re-tools for the rules a template
cannot carry. The gate is sap/audit_roster.py --tier 3: zero WRONG and
zero MISSING rows for tiers 1-3.

Structure credit: colin-cannell's PR #10 (colin/sap2-tier6-roster,
709f7aa) shaped several of these rows and its tier assignments are
right. Where its scrape-sourced values disagreed with the build, the
build won:

  - Rabbit's selector is the EATER (TargetsTriggerTarget), not SELF. An
    Apple onto a different friend buffs that friend, not the Rabbit.
  - Rabbit's three activations are per TURN and reset at the boundary;
    the row needs max_per_turn, not a lifetime cap.
  - the tier-3 FOOD pool includes Birthday Cake. PR #10 omits it, which
    makes every tier-3-and-later food roll draw 1/7 over the wrong set.
  - Badger's "adjacent" is the nearest LIVING body each way on the
    merged grid, not the neighbouring cell and not a fallback to the
    enemy front "when there is no ally ahead": with the enemy front
    mid-faint the splash reaches the body behind it.
  - Badger's and Dodo's percentage is floored, not rounded (7 attack
    deals 3 at L1, 10 at L3), and Dodo's touches attack only.
  - Salad Bowl is played on the BOARD; a PlaySpell naming a target is
    dropped outright. It buffs two RANDOM friends, and sap2 offers it on
    one action encoding rather than five.
  - Dolphin's lowest-health finder breaks a tie at RANDOM (28/32 over 60
    seeds), so it consumes the battle stream.
  - Garlic's floor is 2, not 1: 3 damage lands as 2 and 2 lands as 2.
  - Ox grants Melon, and that perk grant wakes Rabbit - the trigger bus
    counts a perk landing on a friendly as a food played on it. A plain
    buff does not.

Three rules Tier 3 exposed rather than introduced, all now measured:
damage eats the temporary health before the permanent half; a Pilled pet
is mid-faint for the whole of its own faint, so its own splash cannot
pick it; and a lethal hit does not cancel the hurt trigger - what a dying
pet loses is its eligibility as a TARGET, which is why Camel still buffs
and Peacock does not. That replaces this file's old "survival is the
whole gate" reading with one rule instead of two.

Also measured and corrected: two summons onto one cell push the run
behind them back, or the run in front of them forward when there is no
room behind. The previous fallback was marked unmeasured in the code and
was wrong; six Sheep layouts pin it.

The team slot carries two more numbers, for the same reason it already
carries the exp counter: a Birthday Cake's accumulated sell value and
the activations a per-turn-capped ability has spent.

Verified: clang -O2 -Wall -Wextra -Werror clean; 84 env tests (the 72
that were here plus one per Tier-3 rule); audit_roster.py --tier 3 with
zero WRONG and zero MISSING for tiers 1-3; difftest.py 300 --survivors
at tiers 1, 2 and 3, 300 --deterministic and 200 --holes at tiers 1 and
3, all 0 mismatching; difftest_shop.py 26/26; fuzz_shop.py 200 and 120
--turn 5 with no divergence; difftest_match.py with no divergence.
…ot widths

Tier 3 widened a team slot by two floats (sell_bonus, uses) and the
species one-hot by ten. Two consumers decoded the old layout and neither
failed loudly:

- `bots/sap2/greedy` is self-contained by the rule in bots/README.md, so
  its copy of the widths went stale and it spent the whole time reading
  garbage. It did not crash - it just played badly, and the only visible
  symptom was its rating against `random` falling from 1804 to 1521 Elo.
  Its tables now cover all 34 species and the comment says what the
  symptom looks like, because the next roster tier will do this again.
- `tools/visualize_sap2.py` derived the species one-hot width by
  subtracting every other field from the slot width, which over-counted by
  exactly the two new floats, so argmax ran past the end of the one-hot and
  returned species 35 - an id no table has. KeyError on any board that
  reached turn 5.

The second is the interesting one, because the derivation was written to
BE the robust option. It is not: subtracting the known fields from the
total silently absorbs any field you did not know about. So the widths are
now exported - NUM_ALL_SPECIES, NUM_SHOP_SPECIES, NUM_FOODS - and the
consumers read them instead of reconstructing them.

Verified: greedy back to 1838 vs random 1162; the visualizer renders
Tier-3 boards through turn 14 across four seeds with no traceback, with
Badger/Camel/Dodo/Dolphin/Elephant/Giraffe/Ox/Rabbit/Sheep/Ram and
Cake/Garlic/Salad Bowl all appearing; 84 env tests; audit_roster --tier 3
zero WRONG and zero MISSING for tiers 1-3; 300/300 exact surviving
line-ups at tier 1, tier 2 and tier 3; 300/300 tier-3 winners; 200/200
holed tier-3 boards; 26/26 shop checks; fuzz at turn 5 and the
shop-to-battle bridge clean.
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