fix: Hall of the Bandit Lord creature-spell haste mana rider (#5283)#5502
Conversation
There was a problem hiding this comment.
Code Review
This pull request adds support for parsing and applying permanent keyword grants from mana (such as from Hall of the Bandit Lord) by parameterizing the keyword grant with a Duration. The feedback highlights a critical bug in the casting logic where deduplicating keyword grants solely by keyword can discard permanent durations if a temporary duration is processed first.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if !keyword_grants.iter().any(|(k, _)| k == keyword) { | ||
| keyword_grants.push((keyword.clone(), duration.clone())); | ||
| } |
There was a problem hiding this comment.
[HIGH] Deduplicating keyword grants solely by the keyword discards permanent durations if a temporary duration is processed first.
If a creature spell is cast using both a temporary keyword-granting mana (e.g., UntilEndOfTurn from a Dragon-land) and a permanent keyword-granting mana (e.g., Permanent from Hall of the Bandit Lord), and the temporary grant is processed first, the permanent grant will be ignored because the keyword is already present in keyword_grants. The creature will incorrectly lose the keyword at the end of the turn, violating MTG rules.
We should deduplicate by both the keyword and the duration to ensure both distinct durations are applied correctly.
| if !keyword_grants.iter().any(|(k, _)| k == keyword) { | |
| keyword_grants.push((keyword.clone(), duration.clone())); | |
| } | |
| if !keyword_grants.iter().any(|(k, d)| k == keyword && d == duration) { | |
| keyword_grants.push((keyword.clone(), duration.clone())); | |
| } |
References
- Strict fidelity to the MTG Comprehensive Rules (CR) — every game rule, validation, and computed value matches the CR exactly. Discarding permanent keyword grants in favor of temporary ones violates this rule. (link)
Parse changes introduced by this PR · 4 card(s), 3 signature(s) (baseline: main
|
|
CI is red on You added a variant to the mana enum in I have not reviewed the substance yet, and will once CI is green. Two things I did check while I was here, so you know the premise holds:
Ping me when it is green. |
|
CI is green on I tried Please rebase onto current
Nothing else outstanding. |
a171541 to
f521854
Compare
matthewevans
left a comment
There was a problem hiding this comment.
Approved. Thanks for this one.
I verified the card text against Scryfall before reviewing the parse: Hall of the Bandit Lord reads {T}, Pay 3 life: Add {C}. If that mana is spent on a creature spell, it gains haste. There is no "until end of turn" suffix on the rider.
The dedup change is the fix, not a side effect
This is the thing I most want to flag, because your commit message undersells it. You describe the "dedup by duration" change as if it rides along with the mechanical large_enum_variant boxing. It doesn't ride along. It is the consumption half of the fix.
apply_mana_spell_grants in casting.rs previously collected a Vec<Keyword>, deduped on the keyword alone, and applied every grant with a hardcoded Duration::UntilEndOfTurn. A correctly parsed Permanent grant was therefore discarded at the point of use — the parser could produce the right AST all day and the board would never see it. Collecting Vec<(Keyword, Duration)>, deduping on the pair, and applying each grant with its own duration is a widening of that seam, and it is what lets the parser fix actually reach the battlefield. Without those nine lines the parser change would have been inert.
Duration assignment is correct
The parser sets Duration::UntilEndOfTurn only when the "until end of turn" suffix is present, and Duration::Permanent otherwise. Hall therefore grants permanent haste, which matches the printed card.
For your reassurance, since I expect you eyed it: default_mana_keyword_grant_duration() returning UntilEndOfTurn is a serde default for legacy data. It does not sit on the parse path.
The integration test drives the real seam
It builds Hall from Oracle text, activates the mana ability and asserts colorless mana in the pool, casts a creature spell with that mana, and asserts has_keyword(..., &Keyword::Haste) on the resulting battlefield permanent. A mocked grant could not make that pass. The arrow it traces — parsed rider, to spent mana, to keyword on the permanent — runs directly through where the defect lived.
Rules and style
CR 106.6 ("Some spells or abilities that produce mana restrict how that mana can be spent"), CR 702.10, and CR 702.10a are all grep-verified against docs/MagicCompRules.txt and all apt. Nom compliance is clean in the added parser lines.
Ignore the parse-diff sticky — it is a renderer bug, not a regression
Your parse-diff sticky shows four cards (Hall of the Bandit Lord, Arena of Glory, Generator Servant, Domri, Chaos Bringer) losing a grant Haste / grant Riot sub-ability with no compensating addition. Please do not chase this. It reads like a regression and it is not one.
ManaSpellGrant appears zero times in coverage.rs, so the signature renderer has no way to display the grant you attached to the produced mana. Only the removal of the old bogus self-affecting sub-ability is visible to it. The positive half of the change is real, and I verified it through your integration test instead. I have filed #5507 for the renderer gap; it is the third instance of that family, after #5492 and #5495.
Blast radius
Worth calling out: removing the bogus Spell sub-ability with its SelfRef haste static fixes three sibling cards beyond the one in the title. Nice reach for a targeted change.
Thirteen checks green. Approving and enqueuing.
…n the parse-diff signature (phase-rs#5507) (phase-rs#5511) `effect_details` rendered only `produced` for `Effect::Mana`, swallowing `restrictions`/`grants`/`expiry`/`target` with `..`. So attaching a `ManaSpellGrant` to produced mana (Hall of the Bandit Lord's creature-spell haste rider, phase-rs#5502) showed only removals in the coverage-parse-diff sticky with no compensating addition — reading as a regression when it was a half-rendered effect. Third instance of the family (phase-rs#5492/phase-rs#5493 PreventDamage, phase-rs#5495/phase-rs#5501 ChangeZone). Following phase-rs#5501, fully destructure `Effect::Mana` (no `..`) so a new field is a compile error not another silent omission, emitting each only when set so unqualified mana signatures stay byte-identical. Adds mana_signature_exposes_grants. Closes phase-rs#5507. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Hall of the Bandit Lord's activated mana ability was parsed with an incorrect
Spellsub-ability (SelfRefhaste static) instead of aManaSpellGranton the produced mana. Creature spells cast with its colorless mana therefore did not gain haste.Fixes #5283.
Root cause
The mana-rider parser only recognized subtype-specific grants with an explicit
"until end of turn"suffix:"If that mana is spent on a Dragon creature spell, it gains haste until end of turn."Hall of the Bandit Lord prints the broader, permanent form:
"If that mana is spent on a creature spell, it gains haste."parse_conditional_keyword_grantdeclined this clause, so the sequence parser fell through to a genericIfabsorption that lowered haste ontoSelfRef(the land) rather than folding a spend rider intoEffect::Mana.grants.Changes
Parser (
oracle_effect/mana.rs)parse_conditional_keyword_grantto accept:"a creature spell"→OnlyForSpellType("Creature")"until end of turn"suffix →Duration::UntilEndOfTurnvsDuration::PermanentManaGranton the parentEffect::Mana.Types (
types/mana.rs)ManaSpellGrant::AddKeywordUntilEndOfTurnwithduration: Duration(serde-defaultUntilEndOfTurnfor existing Dragon-land cards).Runtime (
game/casting.rs)apply_mana_spell_grantsrespects per-grantdurationinstead of hardcodingUntilEndOfTurn.Tests
parses_hall_of_bandit_lord_creature_spell_haste_grantissue_5283_hall_of_bandit_lord.rs(parse shape + end-to-end cast with Hall mana → haste on battlefield)Test plan
cargo fmt --allcargo test -p engine --lib bandit_lordcargo test -p engine --lib conditional_mana_keyword_grantcargo test -p engine --test integration issue_5283(2/2)Risk notes
card-data.jsonwill move Hall's rider fromsub_abilityintograntsand clear theSwallowedClausewarning.