fix(parser): UNSUPPORTED one-off: Ace's Baseball Bat#4591
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Parse changes introduced by this PR · 15 card(s), 25 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Reviewed current head 6efa7a34ada87b2a6c09c10cd2840b1934f592a6.
[MED] Filtered MustBeBlocked treats already-assigned matching blockers as unavailable even when they can block more creatures. Evidence: crates/engine/src/game/combat.rs:1407 immediately rejects every blocker in assigned_blockers while searching for an available Dalek/Eldrazi, but the existing MustBeBlockedByAll path checks spare block capacity with assigned_count < extra_block_limit(...) at crates/engine/src/game/combat.rs:1496. Why it matters: a matching blocker with an ExtraBlockers allowance can legally also block Ace's Baseball Bat / Slayer's Cleaver, so this validator can accept an illegal declaration that leaves an able matching blocker unused. Suggested fix: mirror the spare-capacity logic for filtered MustBeBlocked { by: Some(_) } availability and add a subtype-filtered test where the matching blocker is already assigned elsewhere but can block an additional creature.
[MED] The mtgish import path still rejects the filtered rule shape this PR now supports. Evidence: crates/mtgish-import/src/schema/types.rs:1854 defines MustBeBlockedByADefender(Box<Permanents>), and the checked-in mtgish data uses it for Ace's Baseball Bat and Slayer's Cleaver, but crates/mtgish-import/src/convert/static_effect.rs:311 only maps bare P::MustBeBlocked; the filtered variant still reaches the UnknownVariant fallback at crates/mtgish-import/src/convert/static_effect.rs:372. Why it matters: the parser/runtime now claims support for these filtered requirements, but regenerated mtgish imports for the same cards still fail to convert. Suggested fix: map P::MustBeBlockedByADefender(filter) to StaticMode::MustBeBlocked { by: Some(filter::convert(filter)?) } and add import coverage for the Dalek/Eldrazi cases.
…mport Resolves both CHANGES_REQUESTED items from @matthewevans: [MED] combat.rs — filtered MustBeBlocked spare-capacity correctness Previously, has_available_blocker for MustBeBlocked { by: Some(filter) } rejected any creature in assigned_blockers regardless of its ExtraBlockers capacity, mirroring a bug that MustBeBlockedByAll already handled correctly. Fix: switch the early-exit from assigned_blockers.contains(id) (all-attacker set) to blockers_per_attacker.get(&attacker_id).is_some_and(|bl| bl.contains(id)) (this-attacker set), and add attackers_per_blocker.get(id) < extra_block_limit() to allow multi-block-capable creatures to count as available — mirroring the MustBeBlockedByAll path at combat.rs:1496. Test: must_be_blocked_filtered_counts_multi_blocker_spare_capacity (Dalek with ExtraBlockers{count:Some(1)} blocking another attacker must still cover the MustBeBlocked attacker; assigned to both is legal). [MED] static_effect.rs — wire MustBeBlockedByADefender in mtgish converter P::MustBeBlockedByADefender(filter) previously fell through to UnknownVariant. Now maps to StaticMode::MustBeBlocked { by: Some(filter::convert(filter)?) }, matching the engine type this PR introduced. Covers Dalek (Ace's Baseball Bat) and Eldrazi (Slayer's Cleaver) cases in the mtgish corpus. CR 509.1c annotation added. Test: must_be_blocked_by_a_defender_lowers_to_filtered_must_be_blocked verifies Dalek and Eldrazi cases convert to MustBeBlocked{by:Some(_)}. Verification: - cargo fmt --all — clean - cargo clippy -p engine --all-targets — exit 0 - cargo clippy -p mtgish-import --all-targets — exit 0 - cargo test -p engine — 14139 passed / 0 failed - cargo test -p mtgish-import (unit tests) — 1 new test passed - check-parser-combinators.sh — no new forbidden patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aQYsGCjiRn71Z4vQDo9QR
Summary
Fixes a parser misparse affecting 1 card(s) in the Doctor Who Commander precons.
Root cause: UNSUPPORTED one-off: Ace's Baseball Bat
Cards corrected
Fix
Implemented cluster 65 by parameterizing the existing
StaticMode::MustBeBlockedinto the data-carryingMustBeBlocked { by: Option<TargetFilter> }(CR 509.1c) — a strict generalization (None = today's bare "must be blocked if able"; Some(filter) = "must be blocked by a if able"). This fixes BOTH Ace's Baseball Bat ("must be blocked by a Dalek if able", conditional on "as long as attacking") and Slayer's Cleaver ("must be blocked by an Eldrazi if able", unconditional), replacing the prior honest-deferral Effect::Unimplemented scaffolding with a fully modeled, rules-enforced typed static. The change is the positive mirror of the existing CantBeBlockedBy { filter } machinery and reuses the same combat-enforcement loop.LAYERS TOUCHED: (1) Type — variant + Hash (discriminant-only)/as_keyword/Display(one-way Debug, mirrors CantBeBlockedBy)/from_str(None). (2) Coverage — added to is_data_carrying_static; removed the now-invalid exact-key registry insert in static_abilities.rs (a non-Hash TargetFilter cannot key a HashMap). (3) Combat — the rules authority: rewrote the MustBeBlocked enforcement block to iterate PER-REQUIREMENT with the optional filter via new helper must_be_blocked_requirements_for_attacker; satisfied = an assigned blocker matching
by(None = any), able = the existing has_available_blocker scan AND matches_target_filter for the Some case. None reproduces today's exact behavior (regression-safe). (4) AI — declare-blocker classification pattern update only (legal-actions already route through the combat validator). (5) Parser — new nom combinator parse_must_be_blocked_by_quality (tag/take_until, the parse IS the detector) + must_be_blocked_quality_to_filter (reuses parse_chosen_qualifier_subject then parse_type_phrase, the same quality combinators CantBeBlockedBy uses) feeding the conditional (shared.rs, gated on the same combat condition), non-conditional (grammar.rs), and standalone (dispatch.rs) paths.VERIFICATION: cargo fmt clean; cargo clippy -p engine --all-targets -D warnings clean; cargo test -p engine 16145 passed / 0 failed (incl. 2 new combat runtime-proof tests proving the parsed MustBeBlocked{by:Some(Dalek)} reaches validate_blockers and rejects a non-Dalek-only block while a Dalek is able, and a building-block parser test for the filter). Regenerated card-data: both cards 0 Unimplemented; Ace's emits MustBeBlocked{by:Some(Typed[Subtype:Dalek])} with condition RecipientMatchesFilter{Attacking} + affected EquippedBy; Slayer's emits MustBeBlocked{by:Some(Typed[Subtype:Eldrazi])} unconditional. semantic-audit: neither card flagged. Task parser diff gate: PASS (no string-dispatch in added lines). check-parser-combinators.sh returns 1 but that is PRE-EXISTING branch debt (it diffs vs ancestor commit ff799f8 and flags lines in files I never edited, e.g. shared.rs
a.split(\", \")); my uncommitted parser diff introduces ZERO forbidden patterns (verified). All CR numbers grep-verified against docs/MagicCompRules.txt.JUDGEMENT CALLS: (a) Hardened the standalone dispatch.rs branch to capture the filtered form too (not just the attached-grant paths) so a hypothetical creature-printed "must be blocked by X" is never silently weakened to bare — never ship a silent misparse. (b) must_be_blocked_quality_to_filter lowercases internally and builds TextPair honestly so it is correct for mixed-case callers (a direct unit test caught a TextPair debug-assert). (c) Restored two data files (crates/engine/data/known-tokens.toml, oracle-subtypes.json) that gen-card-data.sh rewrote as a side effect of the verification step — they are generator artifacts from this worktree's local pool (a pruned token list + an unrelated "Shi-Ar" subtype), not part of the logical change; restored to keep the diff surgical (my own side-effect in an isolated upstream/main worktree, not another agent's work). (d) Fixed the dormant mtgish-import crate's one mechanical construction site to keep the workspace compiling (Boy Scout) without routing anything through mtgish.
NO STOP-AND-RETURN ITEMS: every piece was achievable in-scope; no CR ambiguity, no new subsystem.
Files changed
CR references
Verification
cargo fmt --all— pass./scripts/check-parser-combinators.sh <upstream/main merge-base>— pass (initially failed on a test-assertion .contains() in oracle_static/tests.rs:6537, a file this cluster changed; fixed with a legitimate // allow-noncombinator annotation and committed as 6efa7a3; re-run clean)cargo clippy -p engine --all-targets -- -D warnings— pass (exit 0, clean; fix's own changed files clippy-clean)cargo test -p engine— pass (exit 0, all tests passed)oracle-gen --filter "ace's baseball bat"— pass (AST matches Oracle text; MustBeBlocked by Dalek + Equip legendary creature {1} both modeled; no Unimplemented/Unknown)Cards confirmed re-parsed correctly: Ace's Baseball Bat
🤖 Generated with Claude Code