Skip to content

Fix Vindictive Lich#5530

Merged
matthewevans merged 2 commits into
phase-rs:mainfrom
mike-theDude:fix/4959-vindictive-lich-modal-pipeline
Jul 10, 2026
Merged

Fix Vindictive Lich#5530
matthewevans merged 2 commits into
phase-rs:mainfrom
mike-theDude:fix/4959-vindictive-lich-modal-pipeline

Conversation

@mike-theDude

Copy link
Copy Markdown
Collaborator

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

  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/game/engine_trigger_target_tests.rs

CR references

  • CR 700.2a-b
  • CR 603.3c
  • CR 115.1

Track

Developer

LLM

Model: codex-5
Thinking: high
Tier: Standard

Gate A

./scripts/check-parser-combinators.sh exited 0 with no stdout.

Anchored on

  • crates/engine/src/game/ability_utils.rs:731 — existing modal per-mode target legality filtering derives target constraints and uses target-slot assignment checks.
  • crates/engine/src/game/engine_modes.rs:108 — existing ability-mode selection derives target constraints before routing chosen modes into target selection.
  • crates/engine/src/game/engine.rs:5242 — existing pending-trigger modal cleanup removes illegal no-mode triggers from the stack.

Verification

  • cargo fmt --all -- --check — clean
  • ./scripts/check-parser-combinators.sh — clean
  • cargo test -p engine vindictive_lich — 3 passed / 0 failed
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo test -p engine — 16,045 unit tests passed; 2,588 integration tests passed; 0 failed
  • ./scripts/gen-card-data.sh — clean
  • cargo coverage — Vindictive Lich: supported: true, gap_count: 0, no parser warnings
  • cargo semantic-audit — Vindictive Lich: 0 findings

Scope Expansion

None.

Validation Failures

None.

CI Failures

None.

@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

✓ No card-parse changes detected.

@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.

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.

@matthewevans matthewevans added the bug Bug fix label Jul 10, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jul 10, 2026
Merged via the queue into phase-rs:main with commit 418c597 Jul 10, 2026
12 checks passed
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.

Vindictive Lich — You can select the mode you want but when you click on confirm nothing happen.

3 participants