Skip to content

feat(parser): scry N draw that many instead replacement#3403

Open
glorysr1209-png wants to merge 1 commit into
phase-rs:mainfrom
glorysr1209-png:feat/10-scry-that-many-draw-instead
Open

feat(parser): scry N draw that many instead replacement#3403
glorysr1209-png wants to merge 1 commit into
phase-rs:mainfrom
glorysr1209-png:feat/10-scry-that-many-draw-instead

Conversation

@glorysr1209-png

Copy link
Copy Markdown
Contributor

Summary

Closes 3402. Replacement parser gap: scry N draw that many cards instead.

Changes

  • Extend crates/engine/src/parser/oracle_replacement.rs with focused parser arm + unit test
  • Minimal diff scoped to one high-leverage replacement pattern family

Test plan

  • New #[test] in oracle_replacement.rs covers representative Oracle text
  • CI cargo test -p engine (authoritative gate — not run locally)

@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 adds support for parsing Sphinx-class replacement effects ("If you would scry N, draw that many cards instead") in the oracle parser, along with a corresponding unit test. The feedback highlights two architectural violations of the repository's style guide: first, using scan_contains for parsing dispatch violates rule R1, which requires using nom combinators on the first pass; second, the parser implementation relies on fragile take_until and verbatim string parsing instead of decomposing the phrase into modular, reusable parsers.

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 +278 to +284
if nom_primitives::scan_contains(&lower, "if you would scry ")
&& nom_primitives::scan_contains(&lower, "draw that many cards instead")
{
if let Some(def) = parse_scry_that_many_draw_replacement(&lower, &text) {
return Some(def);
}
}

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

R1. Nom combinators on the first pass — no exceptions

Using scan_contains for parsing dispatch violates the hard architectural rule R1. Parser dispatch should not use string-search helpers like scan_contains or .contains(). Instead, call the parser function directly, allowing it to fail fast via nom combinators.

    if let Some(def) = parse_scry_that_many_draw_replacement(&lower, &text) {
        return Some(def);
    }
References
  1. R1. Nom combinators on the first pass — no exceptions. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. Findings: any new .contains("..."), .starts_with("..."), .ends_with("..."), .find("..."), or .split_once("...") used for parsing dispatch in non-test parser code. (link)

Comment on lines +4353 to +4355
let (rest, _) = tag::<_, _, OracleError<'_>>("if you would scry ").parse(lower).ok()?;
let (rest, _) = take_until::<_, _, OracleError<'_>>(", draw that many cards instead").parse(rest).ok()?;
let (_, _) = tag(", draw that many cards instead").parse(rest).ok()?;

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

Avoid fragile take_until and verbatim string parsing

Using take_until to skip the scry count is fragile and bypasses structured parsing. Additionally, avoid verbatim string equality for parsing Oracle phrases as it creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts (e.g., subjects, conjunctions) and compose them using idiomatic combinator aggregates (like nested alt and tag sequences) to prevent combinatorial explosion and improve maintainability.

    let (rest, _) = parse_if_you_would(lower).ok()?;
    let (rest, _) = parse_scry(rest).ok()?;
    let (rest, _) = nom_primitives::parse_number_or_x.parse(rest).ok()?;
    let (rest, _) = parse_draw_instead(rest).ok()?;
    let (_, _) = opt(char('.')).parse(rest).ok()??;
References
  1. R1. Nom combinators on the first pass — no exceptions. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. (link)
  2. Avoid verbatim string equality for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts (e.g., subjects, conjunctions) and compose them using idiomatic combinator aggregates (like nested alt and tag sequences) to prevent combinatorial explosion and improve maintainability.

@glorysr1209-png glorysr1209-png force-pushed the feat/10-scry-that-many-draw-instead branch from 221f9f5 to 3ce089b Compare June 15, 2026 17:24
@glorysr1209-png glorysr1209-png force-pushed the feat/10-scry-that-many-draw-instead branch from 3ce089b to 6927b1a Compare June 15, 2026 17:31
@matthewevans matthewevans added the enhancement New feature or request label Jun 15, 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.

Blocking this as currently modeled because the new fixed-count antecedent is not represented in the runtime replacement.

Evidence from the actual head 6927b1aa: parse_scry_that_many_draw_replacement consumes "if you would scry ", then uses take_until(", draw that many cards instead") and discards whatever quantity was between those two phrases. The emitted ReplacementDefinition::new(ReplacementEvent::Scry) has no condition for that antecedent count. Runtime evidence: scry_matcher in game/replacement.rs matches every ProposedEvent::Scry { count, .. } where count > 0, and the applier only uses the event count as the replacement amount. So parsing "If you would scry 2, draw that many cards instead" would replace scry 1, scry 3, etc. as well.

The existing parse_scry_count_replacement already covers the supported class ("If you would scry a number of cards, draw that many cards instead") with EventContextAmount. For a fixed-N antecedent, either the engine needs a typed scry-count applicability gate and runtime coverage proving only that N is replaced, or this parser should not claim the pattern yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants