Fix Vindictive Lich#5530
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✓ No card-parse changes detected. |
matthewevans
left a comment
There was a problem hiding this comment.
Approving — reviewed on head e50f5929.
Card + rules verification (live): Vindictive Lich's Oracle text is "When this creature dies, choose one or more. Each mode must target a different player. • Target opponent sacrifices… • Target opponent discards two cards. • Target opponent loses 5 life." (Scryfall). I confirmed the parser emits ModalSelectionConstraint::DifferentTargetPlayers for that exact reminder phrase (oracle_modal.rs), so this fix is not inert — the real card parses to a modal carrying the constraint and the new cap fires on it. The fixture tests hardcode the constraint, but the end-to-end parse path is present.
The bug is real: per-mode legality (filter_modes_by_target_legality) only proves each mode is individually legal; it can't see that DifferentTargetPlayers makes a 3-mode selection impossible in a duel with one opponent. Before this, the engine would let you choose all three then strand target assignment.
Architecture — single authority across all flows: modal_choice_with_target_assignment_limit is built once and called from all three modal-dispatch sites (activate / pending-trigger / dispatch). That's the correct generalization for a 3-flow cluster rather than three ad-hoc caps. It composes existing primitives (build_chained_resolved, build_target_slots, has_legal_target_assignment_for_ability).
Rules mapping is correct per context: activated abilities (casting.rs) return Err when no legal mode (CR 700.2a — activation rejected), while triggered abilities (engine.rs/triggers.rs) drop from the stack (CR 700.2b + CR 603.3c — removed if no mode chosen). That asymmetry is exactly right.
Tests are discriminating: duel caps max_choices to 1, 3-player FFA caps to 2 (two opponents), and over-selection (SelectModes{[0,1]}) is rejected while [0] proceeds to Priority.
Non-blocking (separate issue, pre-existing): the recognizer at oracle_modal.rs uses a verbatim lower == "each mode must target a different player" match, which is the no-verbatim-string-match pattern CLAUDE.md prohibits — but it predates this PR and is out of scope here. Not a blocker.
Solid engine work.
Summary
Fixes the Vindictive Lich modal-trigger softlock by capping modal choices to the largest set whose combined target slots can satisfy modal target constraints.
Closes #4959.
Files changed
CR references
Track
Developer
LLM
Model: codex-5
Thinking: high
Tier: Standard
Gate A
./scripts/check-parser-combinators.shexited 0 with no stdout.Anchored on
Verification
cargo fmt --all -- --check— clean./scripts/check-parser-combinators.sh— cleancargo test -p engine vindictive_lich— 3 passed / 0 failedcargo clippy --all-targets -- -D warnings— cleancargo test -p engine— 16,045 unit tests passed; 2,588 integration tests passed; 0 failed./scripts/gen-card-data.sh— cleancargo coverage— Vindictive Lich:supported: true,gap_count: 0, no parser warningscargo semantic-audit— Vindictive Lich: 0 findingsScope Expansion
None.
Validation Failures
None.
CI Failures
None.