Skip to content

Partial: Batroc the Leaper - #7000

Open
JacobWoodson wants to merge 1 commit into
phase-rs:mainfrom
JacobWoodson:card/batroc-the-leaper
Open

Partial: Batroc the Leaper#7000
JacobWoodson wants to merge 1 commit into
phase-rs:mainfrom
JacobWoodson:card/batroc-the-leaper

Conversation

@JacobWoodson

@JacobWoodson JacobWoodson commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a parse-fidelity defect on Batroc the Leaper.

Issue: Enters-with-counter replacement parses count as Fixed 1 instead of dynamic KickerCount, so "a +1/+1 counter for each time he was kicked" wrongly gives exactly one counter regardless of kicks (parser flagged SwallowedClause/DynamicQty).

Files changed

  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_quantity.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_replacement.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\tests\integration\batroc_the_leaper_multikicker_counters.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\tests\integration\main.rs

CR references

  • CR 702.33c
  • CR 702.33d
  • CR 614.1c
  • CR 614.12
  • CR 201.5

Track

Developer

LLM

Model: claude-opus-4-8
Thinking: high

Tier: Frontier

Verification

  • cargo fmt --all — clean (exit 0)
  • ./scripts/check-parser-combinators.sh — pass with env-skip: Gate G PASS and families A/B/C/E/F verified clean on the parser diff (base 9169d8f); Family-D skipped because python3 is the Windows-Store stub (exit 126, Permission denied) — the documented env limitation, not a failure. The script's raw exit was 1 solely because the Family-D self-test precondition (D0) hard-exits when python3 is unavailable, before the (already-clean) A-F diff scan. My in-loop fixes touched only a test file and crates/engine/src/game/engine.rs (neither under crates/engine/src/parser), so the gate verdict is unaffected.
  • cargo clippy-strict — clean after in-loop fix (exit 0). Initial run failed with clippy::while_let_loop in crates/engine/tests/integration/batroc_the_leaper_multikicker_counters.rs:139 (the Batroc test under verification); converted the loop { match waiting_for {...} } to while let WaitingFor::OptionalCostChoice { .. } = ... (semantics preserved). Re-ran clean.
  • cargo test -p phase-engine — clean after in-loop fix (exit 0; 18489 lib + 4486 integration + doc-tests all pass). Initial run had 1 failure: game::engine::stage2_injector_tests::the_cr_603_5_prompt_census_is_pinned... — a pre-existing Windows path-separator bug UNRELATED to Batroc: actual vs expected producer lists had identical line coordinates and counts (5/7/25, total 37), differing only in \ (from Path::display() at engine.rs:15144) vs hardcoded /. Normalized the separator with .replace('\', "/") without touching the guarded line-coordinate pins. Re-ran clean.
  • cargo export-cards data --stats --sidecar-dir client/public && cp client/public/card-data.json data/card-data.json — clean. Regenerated 98MB card-data against THIS branch's engine (28154 cards scanned), sidecar written to client/public/card-data.json, copied to data/card-data.json (both 98176031 bytes).
  • cargo coverage — ran clean (exit 0; overall 88.94%, 31713/35657) BUT verification criterion FAILED: 'Batroc the Leaper' is supported:false, gap_count:1. NOTE the earlier apparent exit 101 was a SIGPIPE artifact from a head closing the pipe, not a coverage failure; the clean re-run to a file returned 0.
  • cargo semantic-audit — clean (exit 0; 32700 cards audited, 295 with findings). 'Batroc the Leaper' is not in flagged_cards -> 0 findings. semanticAuditClean=true.

Scope Expansion

None.

Validation Failures

See review/cross-check notes.

CI Failures

  • cargo coverage verification criterion NOT met: 'Batroc the Leaper' is supported:false with gap_count:1 (needs supported:true gap:0). The swallow-check flags one swallowed clause: handler=Swallow:DynamicQty, source_text='[warning:swallowed-clause] DynamicQty - Batroc enters with a +1/+1 counter on him for each time he was kicked.' The parser DID parse the enters-with-counter replacement (replacement 'Moved' -> ability 'PutCounter' counter='1 P1P1' target=self) but fixed it at exactly ONE +1/+1 counter and DROPPED the 'for each time he was kicked' kicker-count multiplier, so Batroc would enter with 1 counter instead of N. The other two abilities parse correctly and are supported:true: the ChangesZone/DealDamage trigger (targets='0-kicker payments for this spell', i.e. kicker-count is wired there) and the Multikicker {2} additional cost. NOT fixed in-loop: correctly wiring a dynamic PutCounter quantity keyed to kicker count on the enters-with-counters replacement is a class-level parser/engine change (build-for-the-class dynamic-quantity on the counter-count axis + CR annotation + card-test), which exceeds a 2-retry verification in-loop patch; additionally the relevant parser files (oracle_replacement.rs, oracle_effect/counter.rs, oracle_nom/quantity.rs, oracle_quantity.rs) currently carry uncommitted mid-implementation edits from the card-run pipeline, so per multi-agent safety I did not modify them. This gap should be routed back to the Batroc implementation step (engine-implementer) rather than patched by the verification gate.

Summary by CodeRabbit

  • Bug Fixes

    • Improved parsing of self-referential and object pronouns, including “it,” “him,” “her,” and “them.”
    • Fixed relative path handling across platforms.
    • Improved support for dynamic kicker-based counter quantities and replacement effects.
  • Tests

    • Added coverage for gendered and plural pronouns.
    • Added integration tests for multikicker counter behavior, including zero, multiple, and independent kicks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR centralizes object-recipient pronoun parsing across Oracle parsers, expands kicker-count and replacement parsing, adds Batroc multikicker integration tests, and normalizes relative path separators before pinned-coordinate comparisons.

Changes

Oracle parser updates

Layer / File(s) Summary
Shared recipient-pronoun parser
crates/engine/src/parser/oracle_nom/primitives.rs, crates/engine/src/parser/oracle.rs, crates/engine/src/parser/oracle_nom/condition.rs, crates/engine/src/parser/oracle_effect/counter.rs, crates/engine/src/parser/oracle_nom/quantity.rs
Adds parse_object_recipient_pronoun and uses it for it, them, him, and her parsing. Reflexive forms and existing ~ handling remain supported.
Replacement and kicker-count parsing
crates/engine/src/parser/oracle_replacement.rs, crates/engine/src/parser/oracle_quantity.rs
Expands self-referential replacement clauses and kicker-count subjects. Adds parser tests for gendered and plural pronouns, invalid verbs, trailing text, and existing creature-count parsing.
Batroc multikicker integration coverage
crates/engine/tests/integration/batroc_the_leaper_multikicker_counters.rs, crates/engine/tests/integration/main.rs
Adds real cast-pipeline tests for two kicks, zero kicks, and isolation from another permanent’s kicker count. Registers the integration test module.

Path normalization

Layer / File(s) Summary
Relative path separator normalization
crates/engine/src/game/engine.rs
Replaces backslashes with forward slashes before pinned-coordinate census comparisons.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: matthewevans

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the affected card and accurately indicates that the pull request provides a partial fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
crates/engine/src/parser/oracle_quantity.rs (2)

3508-3535: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider sharing the "was/were kicked" verb-anchor scan with parse_where_x_kicker_count.

The comment above parse_for_each_kicker_count states it "mirrors the permissive subject match already used by oracle_effect::lower::parse_where_x_kicker_count." Two independent implementations of the same "subject was/were kicked" grammar can drift apart over time (for example, if one recognizer later needs a new subject form and the other does not get updated). Extract the shared take_until(" was kicked") / take_until(" were kicked") scan into one combinator in a shared primitives module, and have both recognizers call it.

This mirrors the single-authority principle this PR already applies to parse_object_recipient_pronoun.
[medium_effort_and_high_reward]

Run the following script to confirm the duplication and locate the sibling function:

#!/bin/bash
# Description: Locate parse_where_x_kicker_count and compare its verb-anchor grammar
# to parse_for_each_kicker_count.
rg -n -B3 -A25 'fn parse_where_x_kicker_count' crates/engine/src/parser
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_quantity.rs` around lines 3508 - 3535,
Extract the shared “was kicked”/“were kicked” subject scan from
parse_for_each_kicker_count and parse_where_x_kicker_count into a reusable
combinator in the shared parser primitives module. Update both recognizers to
call this single combinator while preserving full-consumption validation and
QuantityRef::KickerCount behavior.

4150-4154: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix the CR 201.5 citation.

CR 201.5 governs name-based self-reference, while this test documents grammatical pronoun agreement for kicker state ("he was kicked" / "it was kicked"). No rule codifies pronoun gender/number as kicker-count equality, so keep the test comment limited to CR 702.33c-d, or cite the actual governing source if one exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_quantity.rs` around lines 4150 - 4154, Update
the comment in for_each_time_gendered_pronoun_was_kicked_maps_to_kicker_count to
remove the incorrect CR 201.5 citation and describe the test as governed by CR
702.33c-d only; do not imply that the rules explicitly establish gendered/neuter
pronoun kicker-count equality.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/parser/oracle_nom/primitives.rs`:
- Around line 189-195: Update the documentation comment above the anaphoric
pronoun handling in the relevant parser code to remove the inaccurate CR
608.2c/608.2k citations and unsupported claims about earlier clauses and pronoun
categories. Either narrow the comment to behavior directly supported by a
verified Comprehensive Rules citation or replace it with a verified rule whose
text matches the implementation, while preserving the explanation that these
pronouns resolve to the established referent.
- Around line 203-205: Update parse_object_recipient_pronoun to require a word
boundary after each pronoun, preventing matches for prefixes such as item and
itself while preserving valid pronoun parsing. Ensure callers such as
parse_counter_object_scope still require full-clause consumption, and add
regression coverage for both rejected inputs.

---

Nitpick comments:
In `@crates/engine/src/parser/oracle_quantity.rs`:
- Around line 3508-3535: Extract the shared “was kicked”/“were kicked” subject
scan from parse_for_each_kicker_count and parse_where_x_kicker_count into a
reusable combinator in the shared parser primitives module. Update both
recognizers to call this single combinator while preserving full-consumption
validation and QuantityRef::KickerCount behavior.
- Around line 4150-4154: Update the comment in
for_each_time_gendered_pronoun_was_kicked_maps_to_kicker_count to remove the
incorrect CR 201.5 citation and describe the test as governed by CR 702.33c-d
only; do not imply that the rules explicitly establish gendered/neuter pronoun
kicker-count equality.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dd077b02-f69d-4079-bd47-81f6b6e6ff15

📥 Commits

Reviewing files that changed from the base of the PR and between b5ab421 and 84d10f5.

📒 Files selected for processing (10)
  • crates/engine/src/game/engine.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_effect/counter.rs
  • crates/engine/src/parser/oracle_nom/condition.rs
  • crates/engine/src/parser/oracle_nom/primitives.rs
  • crates/engine/src/parser/oracle_nom/quantity.rs
  • crates/engine/src/parser/oracle_quantity.rs
  • crates/engine/src/parser/oracle_replacement.rs
  • crates/engine/tests/integration/batroc_the_leaper_multikicker_counters.rs
  • crates/engine/tests/integration/main.rs

Comment on lines +189 to +195
/// CR 608.2c + CR 608.2k: these pronouns are anaphors that bind to an object
/// established earlier in the same ability (its trigger, cost, or an earlier
/// clause) rather than a newly-parsed target. Gendered singulars (`him`/`her`)
/// appear on named creatures that self-reference (e.g. Batroc the Leaper's
/// "a +1/+1 counter on him"); `them` covers a distributive subject or a
/// gender-neutral singular. The engine never inspects which pronoun was used —
/// it only needs the anaphoric binding — so the whole set maps to one referent.

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace the incorrect Comprehensive Rules citation.

CR 608.2c requires following instructions in written order. It does not define anaphoric object binding. CR 608.2k covers a specific untargeted object previously referred to by an ability’s cost or trigger condition. It does not support the full claim about any earlier clause and pronoun mapping. (media.wizards.com)

Narrow the comment to the verified rule behavior, or cite a verified rule that supports the exact claim.

As per path instructions, rules-touching engine code must use a verified CR citation whose rule body matches the implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_nom/primitives.rs` around lines 189 - 195,
Update the documentation comment above the anaphoric pronoun handling in the
relevant parser code to remove the inaccurate CR 608.2c/608.2k citations and
unsupported claims about earlier clauses and pronoun categories. Either narrow
the comment to behavior directly supported by a verified Comprehensive Rules
citation or replace it with a verified rule whose text matches the
implementation, while preserving the explanation that these pronouns resolve to
the established referent.

Source: Path instructions

Comment on lines +203 to +205
pub fn parse_object_recipient_pronoun(input: &str) -> OracleResult<'_, &str> {
alt((tag("it"), tag("them"), tag("him"), tag("her"))).parse(input)
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-token prefixes in the shared pronoun parser.

tag("it") succeeds on item and itself, leaving the suffix for the caller. parse_counter_object_scope calls this parser without a boundary guard, so a non-pronoun prefix can enter ObjectScope::Source. Enforce a word boundary here or at every caller. Add regression cases for item and itself.

As per path instructions, parser code must preserve full-clause consumption so unsupported tails are not silently swallowed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_nom/primitives.rs` around lines 203 - 205,
Update parse_object_recipient_pronoun to require a word boundary after each
pronoun, preventing matches for prefixes such as item and itself while
preserving valid pronoun parsing. Ensure callers such as
parse_counter_object_scope still require full-clause consumption, and add
regression coverage for both rejected inputs.

Source: Path instructions

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

High — Kicker-count grammar accepts unrelated or empty subjects

parse_for_each_kicker_count in crates/engine/src/parser/oracle_quantity.rs:3508-3534 discards every token between time and was/were kicked, then always emits QuantityRef::KickerCount. The resolver in crates/engine/src/game/quantity.rs:4395-4406 has no subject parameter: it reads only the trigger or self source's kickers_paid. Consequently, unsupported inputs such as for each time a creature was kicked (and an empty subject) silently resolve to the source's kick count instead of remaining explicit unsupported syntax or carrying an authoritative subject.

Please put this grammar in crates/engine/src/parser/oracle_nom/quantity.rs as an all-consuming, word-boundary-safe self-subject combinator, and delegate to it from the semantic entry point. It should admit only established self forms (it, this spell, ~, plus a correctly bound named-self pronoun if the existing architecture supports that); non-self and empty subjects must be rejected. Add negative reach-guard coverage for a generic creature and an empty subject, while retaining the Batroc runtime behavior for the real self form.

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

MED — CR annotation overclaims the pronoun-reference rule

crates/engine/src/parser/oracle_nom/primitives.rs:189-195 attributes the broad statement that these pronouns bind objects introduced by a trigger, cost, or earlier clause to CR 608.2c + CR 608.2k. The verified rules do not support that scope: CR 608.2c governs instruction ordering and applying the rules of English, while CR 608.2k is limited to a specific untargeted object previously referred to by that ability's cost or trigger condition across characteristic changes.

Please remove or narrow the CR claim to its verified scope. The generic parser-grammar explanation may remain, but it should not assert that those CR sections define the full anaphora behavior implemented here.

@matthewevans matthewevans removed their assignment Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Generated for head 84d10f5a68df1a2f7e915e1ad744f422bb5b23b3.

Parse changes introduced by this PR · 4 card(s), 4 signature(s) (baseline: main b5ab4215ac2f)

🟡 Modified fields (4 signatures)

  • 1 card · 🔄 ability/Draw · changed field count: kicker payments for this spell
    • Affected (first 3): Spell Contortion
  • 1 card · 🔄 ability/PutCounter · changed field counter: 1 P1P1KickerCount P1P1
    • Affected (first 3): Batroc the Leaper
  • 1 card · 🔄 ability/PutCounter · changed field counter: 1 P1P1ObjectCount { filter: Typed(TypedFilter { type_filters: [Land], controller: Some(You), properties: [] }) } P1P1
    • Affected (first 3): Michelangelo, On the Scene
  • 1 card · 🔄 ability/PutCounter · changed field counter: 1 loyaltySpellsCastThisTurn { scope: Controller, filter: Some(Or { filters: [Typed(TypedFilter { type_filters: [Instant], contro…
    • Affected (first 3): Ral, Leyline Prodigy

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

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

Labels

bug Bug fix needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants