Skip to content

feat(engine): register CantBeBlockedUnlessAllBlock in static registry#5645

Merged
matthewevans merged 1 commit into
phase-rs:mainfrom
Whovencroft:card/tromokratis-2
Jul 12, 2026
Merged

feat(engine): register CantBeBlockedUnlessAllBlock in static registry#5645
matthewevans merged 1 commit into
phase-rs:mainfrom
Whovencroft:card/tromokratis-2

Conversation

@Whovencroft

Copy link
Copy Markdown
Contributor

Summary

Closes the last remaining coverage gap for Tromokratis by registering StaticMode::CantBeBlockedUnlessAllBlock in the static-abilities registry and wiring the coverage classifier to recognise it as a supported variant.

Card

Tromokratis — {5}{U}{U} Legendary Creature — Kraken (8/8 )

Tromokratis has hexproof unless it's attacking or blocking.
Tromokratis can't be blocked unless all creatures defending player controls block it.
Whenever Tromokratis becomes blocked, tap all creatures defending player controls.

What already existed

Component Status Location
Parser ✅ implemented oracle_static/evasion.rs:1769parse_block_unless_all_block_nom
Combat enforcement ✅ implemented combat.rs:1574–1655 — declare-blockers validation
Integration tests ✅ registered tromokratis.rs (6 tests) in main.rs:906

What this PR adds

File Change
static_abilities.rs Register CantBeBlockedUnlessAllBlock in build_static_registry() as a handle_rule_mod entry (nullary marker, same pattern as Goaded/CantCrew)
coverage.rs Add text-anchor arm: StaticMode::CantBeBlockedUnlessAllBlock => effective_lower.contains("can't be blocked")
coverage.rs Add regression test cant_be_blocked_unless_all_block_has_no_coverage_gap() asserting zero gaps for a face carrying this static with the Tromokratis oracle text

Design rationale

CantBeBlockedUnlessAllBlock is a nullary (no payload) StaticMode variant. Its runtime semantics are entirely handled by combat.rs declare-blockers validation — the registry entry only needs to be handle_rule_mod (the no-op handler that marks the variant as "known and supported" for the layer system). This mirrors the pattern used for Goaded, CantCrew, MustAttack, and other passive combat-restriction markers.

The coverage classifier uses the "can't be blocked" text anchor (same as CantBeBlocked, CantBeBlockedExceptBy, CantBeBlockedBy) since all four share the same oracle-text prefix. The "unless all creatures" clause specificity is validated by the parser unit tests.

Testing

  • Unit test: cant_be_blocked_unless_all_block_has_no_coverage_gap — constructs a synthetic face with the Tromokratis oracle text and asserts card_face_gaps() returns empty
  • Integration tests (pre-existing, already green): 6 tests in tromokratis.rs exercising the full combat path

Checklist

  • cargo check --lib -p engine — zero errors/warnings
  • rustfmt --check — clean
  • scripts/check-parser-combinators.sh — Gate A PASS
  • No unwrap() / expect() in non-test code
  • No todo!() / unimplemented!() in non-test code
  • CR reference in commit message

CR: 509.1b

Wire the nullary StaticMode::CantBeBlockedUnlessAllBlock into
build_static_registry() as a handle_rule_mod entry (mirrors Goaded,
CantCrew, and other passive combat-restriction markers). Add the
text-anchor arm in coverage.rs so the coverage classifier recognises
the variant as supported. Add a regression test that asserts zero
coverage gaps for a face carrying this static with the Tromokratis
oracle text.

The runtime enforcement (combat.rs declare-blockers validation) and
parser (oracle_static/evasion.rs) already exist; the integration
tests in tromokratis.rs (already registered in main.rs) exercise the
end-to-end path. This commit closes the last remaining coverage gap
for Tromokratis.

CR: 509.1b
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main 9c8de549a93a)

1 card(s) · static/CantBeBlockedUnlessAllBlock · support: falsetrue

Examples: Tromokratis

@matthewevans

Copy link
Copy Markdown
Member

Clean — approve on settled green. This is a coverage false-negative fix, and the diagnosis is right.

✅ Clean

  • The registry insert is load-bearing, not cosmetic. is_static_supported (coverage.rs:6654) gates on static_registry.contains_key(&stat.mode). Combat enforcement, by contrast, runs through state.static_mode_presence (combat.rs:755) and a direct def.mode scan (:1591) — neither consults the registry. So Tromokratis's blocking restriction worked end-to-end while the coverage report called it an unsupported gap. Registering it closes that gap without touching gameplay.
  • handle_rule_mod is the right building block, already established for this exact class — ~20 siblings register through it (CantAttack, CantBlock, MustAttack, MustBlock, CantBeTargeted, Goaded, CantTap, ExtraBlockers…): rule-modification statics whose runtime enforcement lives elsewhere. CantBeBlockedUnlessAllBlock is squarely in that class. No new machinery.
  • No double-apply risk. StaticEffect::RuleModification has zero consumers outside static_abilities.rs — it is a pure marker, so the new registry entry cannot interfere with combat.rs's enforcement.
  • CR 509.1b verified and governing, not merely real: "…effects that say a creature can't block, or that it can't block unless some condition is met. If any restrictions are being disobeyed, the declaration of blockers is illegal." That is precisely Tromokratis.
  • The test discriminates for what the PR changes. Revert the registry insert → is_static_supported goes false → card_face_gaps reports a gap → the test fails. And gameplay is already covered end-to-end by tests/integration/tromokratis.rs ("partial block is illegal"), so there is no gap here to backfill.

🟡 Non-blocking

  • Label will be bug, not feat. The title says feat(engine), but nothing new becomes possible — a supported static was being misreported as unsupported. That's a defect in the coverage report (it understates support and misdirects the parser-gap tooling). Labels classify behavior, not author intent; I'll apply bug on merge.

Recommendation: approve and enqueue once CI settles green. Required checks (Rust lint, both test shards, Card data) are still in flight, and the branch is BEHIND — I don't sign off on a green I haven't watched. Nothing here is on you; this re-surfaces on the next sweep.

@matthewevans matthewevans self-assigned this Jul 12, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — coverage false-negative fix, verified runtime-inert.

✅ Clean

  • Right seam. handle_rule_mod is the established building block for nullary registry-keyed statics (~20 siblings register through it). No new machinery, no new enum surface.
  • Runtime-inert by construction — no double-apply risk. static_registry is read only in coverage.rs. Combat enforcement of this static runs through static_kind_present (combat.rs:755) and a direct def.mode scan (combat.rs:1591), neither of which consults the registry. StaticEffect::RuleModification is constructed only in static_abilities.rs and consumed nowhere — it is a pure marker. Registering the handler therefore cannot change combat behavior; it only corrects the coverage classification.
  • CR 509.1b grep-verified and governing (docs/MagicCompRules.txt:2345): "The defending player checks each creature they control to see whether it's affected by any restrictions (effects that say a creature can't block, or that it can't block unless some condition is met)." Tromokratis is precisely the unless-condition case.
  • Discriminating test. cant_be_blocked_unless_all_block_has_no_coverage_gap fails on main, where the registry lacks the key and card_face_gaps reports a gap.
  • Parse-diff tight and fresh. 1 card (Tromokratis), static/CantBeBlockedUnlessAllBlock support false → true, baseline main 9c8de549a93a, zero collateral. Diff is purely additive (+33/-0).

Labeled bug rather than enhancement despite the feat(engine): title: nothing new becomes possible at runtime — Tromokratis already worked end-to-end while coverage reported it an unsupported gap. This fixes the false negative. Labels classify behavior, not author intent.

@matthewevans matthewevans added the bug Bug fix label Jul 12, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jul 12, 2026
@matthewevans matthewevans removed their assignment Jul 12, 2026
Merged via the queue into phase-rs:main with commit 8dbca3e Jul 12, 2026
12 checks passed
@Whovencroft
Whovencroft deleted the card/tromokratis-2 branch July 16, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants