Skip to content

Feat/demon of fates design alt cost#5856

Merged
matthewevans merged 7 commits into
phase-rs:mainfrom
Whovencroft:feat/demon-of-fates-design-alt-cost
Jul 16, 2026
Merged

Feat/demon of fates design alt cost#5856
matthewevans merged 7 commits into
phase-rs:mainfrom
Whovencroft:feat/demon-of-fates-design-alt-cost

Conversation

@Whovencroft

Copy link
Copy Markdown
Contributor

Summary

Implements the missing static_structure effect for Demon of Fate's Design:

Once during each of your turns, you may cast an enchantment spell by paying life equal to its mana value rather than paying its mana cost.

This adds a new parser function that lowers the Oracle text into a CastWithAlternativeCost static with PayLife { SelfManaValue } cost and OncePerTurn frequency, scoped to enchantment spells.

Rules References

  • CR 118.9 — Alternative costs
  • CR 601.2b — Casting permissions and alternative cost grants

Implementation

File Change
crates/engine/src/parser/oracle_static/cost_mod.rs New parse_cast_by_paying_life_alt_cost() function
crates/engine/src/parser/oracle_static/static_helpers.rs Promote strip_cost_mod_spell_noun_suffix to pub(super)
crates/engine/src/parser/oracle_static/mod.rs Remove unnecessary pub(crate) re-export
crates/engine/src/parser/oracle_static/dispatch.rs Call before try_parse_cast_free_permission
crates/engine/src/parser/oracle_static/tests.rs 2 parser unit tests (positive + negative guard)
crates/engine/tests/integration/main.rs Register integration test module
crates/engine/tests/integration/demon_of_fates_design.rs 3 integration tests

Parser Function

Pattern: [Once during each of your turns, ]you may cast [filter] by paying life equal to its mana value rather than paying its mana cost.

Output:

StaticMode::CastWithAlternativeCost {
    cost: AbilityCost::PayLife {
        amount: QuantityExpr::Ref { qty: QuantityRef::SelfManaValue },
    },
    affected: /* enchantment spell filter */,
    frequency: CastFrequency::OncePerTurn,
}

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces parsing support and integration tests for alternative-cost grants that allow casting spells by paying life equal to their mana value, specifically supporting Demon of Fate's Design. The feedback highlights two key improvements: verifying that the remainder of parse_type_phrase is empty to prevent over-broad alternative cost matching, and replacing a multiline string literal in the tests with concat! to avoid fragile trailing whitespaces that could be stripped by IDEs or formatters.

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.

Comment on lines +718 to +722
let base_filter = if filter_for_parse.is_empty() {
TargetFilter::Typed(TypedFilter::card())
} else {
parse_type_phrase(filter_for_parse).0
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

[HIGH] Unchecked remainder from type phrase parsing. Evidence: crates/engine/src/parser/oracle_static/cost_mod.rs:718.
Why it matters: Ignoring the unconsumed remainder of parse_type_phrase can lead to silent parsing bugs where unsupported or unmodeled qualifiers in the spell filter are dropped, resulting in an over-broad alternative cost grant.
Suggested fix: Verify that the remainder of parse_type_phrase is empty, returning None otherwise.

    let base_filter = if filter_for_parse.is_empty() {
        TargetFilter::Typed(TypedFilter::card())
    } else {
        let (filter, remainder) = parse_type_phrase(filter_for_parse);
        if !remainder.trim().is_empty() {
            return None;
        }
        filter
    };
References
  1. Strict fidelity to the MTG Comprehensive Rules (CR) — every game rule, validation, and computed value matches the CR exactly. Convenience shortcuts that get rules wrong are not simpler; they are wrong. (link)

Comment on lines +18 to +20
const DEMON_ORACLE: &str = "Once during each of your turns, you may cast an \
enchantment spell by paying life equal to its mana value rather than paying \
its mana cost.";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

[MEDIUM] Fragile trailing whitespace in multiline string literal. Evidence: crates/engine/tests/integration/demon_of_fates_design.rs:18.
Why it matters: Trailing whitespaces before line-continuation backslashes are easily stripped by IDEs or formatters on save, which would silently merge words (e.g., 'anenchantment') and break the parser match.
Suggested fix: Use concat! to safely split the string across multiple lines.

Suggested change
const DEMON_ORACLE: &str = "Once during each of your turns, you may cast an \
enchantment spell by paying life equal to its mana value rather than paying \
its mana cost.";
const DEMON_ORACLE: &str = concat!(
"Once during each of your turns, you may cast an ",
"enchantment spell by paying life equal to its mana value rather than paying ",
"its mana cost."
);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba8122eac5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +670 to +674
let (tp, frequency) = match parse_alt_cost_frequency_prefix(tp.lower) {
Ok((rest_lower, freq)) => {
let consumed = tp.lower.len() - rest_lower.len();
(TextPair::new(&tp.original[consumed..], rest_lower), freq)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the 'your turn' gate on the alternative cost

When the printed prefix is Once during each of your turns, this path strips the whole prefix and keeps only CastFrequency::OncePerTurn; granted_spell_alternative_cost_for later checks only that frequency and the affected filter. If another effect lets the controller cast enchantments at instant speed on an opponent's turn, Demon will incorrectly offer the pay-life alternative cost even though the Oracle text limits it to that player's own turns. Thread a StaticCondition::DuringYourTurn (or equivalent timing gate) in addition to the per-turn frequency for this prefix.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 2 card(s), 3 signature(s) (baseline: main 0d39fd307105)

2 card(s) · ability/static_structure · removed: static_structure

Examples: Access Maze, Demon of Fate's Design

1 card(s) · static/CastWithAlternativeCost(PayLife { amount: Ref { qty: SelfManaValue } }) · added: CastWithAlternativeCost(PayLife { amount: Ref { qty: SelfManaValue } }) (affects=in hand you control card, conditional=during your turn)

Examples: Access Maze

1 card(s) · static/CastWithAlternativeCost(PayLife { amount: Ref { qty: SelfManaValue } }) · added: CastWithAlternativeCost(PayLife { amount: Ref { qty: SelfManaValue } }) (affects=you control enchantment, conditional=during your turn)

Examples: Demon of Fate's Design

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans matthewevans self-assigned this Jul 16, 2026
@matthewevans matthewevans added the bug Bug fix label Jul 16, 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.

Request changes — parse timing semantics, not input length.

🟡 Finding

[MED] The cost modifier uses consumed > 20 as a timing proxy. Evidence: crates/engine/src/game/cost_mod.rs:675. Why it matters: timing behavior depends on incidental prefix length. Suggested fix: parse and carry the explicit timing condition; add an Access Maze regression.

Recommendation: request-changes.

@matthewevans matthewevans removed their assignment Jul 16, 2026
Whovencroft added 6 commits July 16, 2026 02:27
CR 118.9 + CR 601.2b: Parse 'Once during each of your turns, you may cast
an enchantment spell by paying life equal to its mana value rather than
paying its mana cost.' into a CastWithAlternativeCost static with
PayLife { SelfManaValue } cost and OncePerTurn frequency.

Implementation:
- Add parse_cast_by_paying_life_alt_cost() to cost_mod.rs
- Export from oracle_static/mod.rs
- Call from dispatch.rs before try_parse_cast_free_permission
- Parser unit tests (positive + negative guard)
- Integration tests (offer, accept+pay, non-enchantment rejection)
…alt_cost

dispatch.rs already imports it via `use super::cost_mod::*`, so the
pub(crate) re-export in mod.rs is unnecessary and triggers -Dunused-imports.
- Replace .strip_suffix(" spells") / .strip_suffix(" spell") with
  strip_cost_mod_spell_noun_suffix() to satisfy the parser combinator gate.
- Fund P0's mana pool in the non-enchantment test so the cast pipeline
  can start (previously panicked with 'Cannot pay mana cost').
…e access

The function was private in static_helpers.rs but cost_mod.rs (a sibling
module) needs to call it via the support re-export. Promote to pub(super).
1. Check parse_type_phrase remainder is empty before accepting the filter
   (prevents over-broad matching on unsupported qualifiers).
2. Replace backslash-continuation string literal with concat!() to avoid
   fragile trailing whitespace that IDEs may strip.
3. Thread StaticCondition::DuringYourTurn when the frequency prefix is
   "once during each of your turns, " so the grant only functions on
   the controller's turn (prevents interaction with instant-speed flash
   enablers on opponents' turns).
4. Add assertion in parser unit test for the new condition field.
Address maintainer review (matthewevans):
1. Replace consumed>20 heuristic with explicit nom tag parse of
   "once during each of your turns, " to determine DuringYourTurn
   condition. The timing gate is now derived from parsing the actual
   prefix variant, not from incidental string length.
2. Add Access Maze regression test: "a spell from your hand" parses
   with InZone::Hand zone qualifier and card() base filter, confirming
   the zone qualifier stripping works correctly.
3. Handle "from your hand" zone qualifier in the filter phrase by
   stripping it before the spell-noun suffix and threading it as
   FilterProp::InZone { zone: Zone::Hand } through
   apply_spell_keyword_subject_constraints.
@Whovencroft Whovencroft force-pushed the feat/demon-of-fates-design-alt-cost branch from e349541 to 147a1d5 Compare July 16, 2026 02:27
@matthewevans matthewevans self-assigned this Jul 16, 2026
Co-authored-by: Whovencroft <6600560+Whovencroft@users.noreply.github.com>

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

Current head review: prior timing blocker is resolved by explicit prefix parsing and Access Maze coverage. Maintainer port retained independent main test additions; parser gates pass. Approving and queueing.

@matthewevans matthewevans enabled auto-merge July 16, 2026 02:48
@matthewevans matthewevans removed their assignment Jul 16, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 16, 2026
Merged via the queue into phase-rs:main with commit 0474516 Jul 16, 2026
12 checks passed
@Whovencroft Whovencroft deleted the feat/demon-of-fates-design-alt-cost 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.

2 participants