Skip to content

fix(parser): route bare "you don't own" ownership suffix (Agent of Treachery)#3435

Open
galuis116 wants to merge 1 commit into
phase-rs:mainfrom
galuis116:fix/agent-of-treachery-not-owned-condition-3304
Open

fix(parser): route bare "you don't own" ownership suffix (Agent of Treachery)#3435
galuis116 wants to merge 1 commit into
phase-rs:mainfrom
galuis116:fix/agent-of-treachery-not-owned-condition-3304

Conversation

@galuis116

Copy link
Copy Markdown
Contributor

Summary

Fixes #3304.

Agent of Treachery — "At the beginning of your end step, if you control three or more permanents you don't own, draw three cards." — drew three cards every end step, because the intervening-if condition was silently dropped (the trigger parsed with condition: null).

Root cause

parse_ownership_or_controller_suffix only recognized the "but don't own" family (which additionally requires a controller already set). The bare "you don't own" suffix had no arm, so parse_type_phrase("permanents you don't own") left "you don't own" unconsumed → parse_control_count_ge returned a non-empty remainder → try_extract_intervening's boundary check failed → the entire condition was discarded.

Fix (parser-only; no new engine variant)

Add a bare "you don't own" / "you do not own" arm to parse_ownership_or_controller_suffix that pushes the existing FilterProp::Owned { controller: Opponent } — runtime-evaluated as owner != controller, i.e. "you don't own it". The arm deliberately does not set the controller (ownership is independent of control, CR 109.4); for "you control N permanents you don't own" the controller is supplied upstream by inject_controller_you, composing to Typed(Permanent, controller: You, [Owned{Opponent}, InZone Battlefield]) = "permanents you control but don't own".

The end-step trigger now carries QuantityComparison(ObjectCount{…} >= 3) and is gated at detection and resolution per CR 603.4 — so it draws only when the controller controls ≥3 permanents they don't own. The "but don't own" path (Thieving Amalgam family) is a separate code path and is untouched.

Runtime

  • Control ≥3 not-owned (e.g. stolen) permanents at your end step → trigger fires, draw 3.
  • Control <3 → condition false → no draw.

The fix consumes only previously-orphaned text; it also lets Sentinel of Lost Lore / similar "card you don't own …" object phrases consume the suffix (strictly additive — the parallel "you own" arm already exists).

Tests

  • agent_of_treachery_end_step_draw_is_gated_by_not_owned_count — the end-step trigger's condition is Some(QuantityComparison{ GE, Fixed 3, ObjectCount{ filter } }) (not None), with the filter Typed(Permanent, controller: You) carrying FilterProp::Owned{ Opponent }; the execute body still draws; no parse warnings.
  • parse_control_count_ge_permanents_you_dont_own — combinator unit (both "don't"/"do not"), empty remainder, Owned{Opponent} + controller: You.
  • parse_type_phrase_but_dont_own_shape_unchanged_by_bare_arm — no-regression: "creature you control but don't own" still yields the unchanged And[Typed(You), Not(Owned{You})] shape (proves the bare arm is additive).

(Reverting the new arm makes the first two tests fail with the exact dropped-condition signature; the no-regression test still passes.)

Verification

  • cargo +nightly test -p engine --lib "own": 297 passed; "dont_own": 8 passed; --test integration: 1065 passed (no fixture diff).
  • cargo +nightly clippy -p engine --lib -- -D warnings: clean. rustfmt --check: clean. Parser-combinator gate: clean (the new arm uses only tag/alt).

CR references grep-verified against docs/MagicCompRules.txt: 108.3 (owner), 109.4 (control ≠ ownership), 603.4 (intervening-if).

…eachery)

"At the beginning of your end step, if you control three or more
permanents you don't own, draw three cards." dropped the intervening-if:
parse_ownership_or_controller_suffix only recognized the "but don't own"
family (which requires a controller already set), so the bare
"you don't own" suffix was left unconsumed, parse_control_count_ge
returned a non-empty remainder, and try_extract_intervening discarded the
whole condition — the trigger drew three cards every end step
unconditionally.

Add a bare "you don't own" / "you do not own" arm to
parse_ownership_or_controller_suffix that pushes the existing
FilterProp::Owned { controller: Opponent } (runtime-evaluated as
owner != controller, i.e. "you don't own it"). It does not set the
controller — ownership is independent of control (CR 109.4); for
"you control N permanents you don't own" the controller is supplied
upstream by inject_controller_you, composing to
Typed(Permanent, controller: You, [Owned{Opponent}]). The end-step
trigger now carries QuantityComparison(count >= 3) and is gated per
CR 603.4. Parser-only; no new engine variant; the "but don't own"
path (Thieving Amalgam family) is untouched.

CR 108.3, CR 109.4. Adds parser + combinator tests asserting the
condition is carried (not dropped) and a no-regression test for the
"but don't own" shape.

Fixes phase-rs#3304
@galuis116 galuis116 requested a review from matthewevans as a code owner June 16, 2026 00:23
@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!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant