Fix Lictor opponent-scoped "entered this turn" intervening-if#6554
Conversation
Lictor's Pheromone Trail — "When this creature enters, if a creature
entered the battlefield under an opponent's control this turn, create a
3/3 green Tyranid Warrior creature token with trample" — dropped its
intervening-"if" condition (parsed to None), so the ETB trigger fired
unconditionally and the token was created every time Lictor entered.
The "under your control" surface of this class was already supported;
this adds the opponent-scoped, past-tense mirror. A new
`parse_entered_this_turn_under_opponent_control` combinator reuses the
shared `parse_or_more_entered_count` / `parse_entered_this_turn_subject`
helpers and carries the scope via `PlayerScope::Opponent { Max }` — the
existential "an opponent" reading already documented on
`parse_opponent_had_entered_this_turn` — over the CR 608.2i
`BattlefieldEntriesThisTurn` snapshot. No new engine variant.
The opponent surface in the QUANTITY path stays honestly Unimplemented
(no printed card); only the condition path, which Lictor uses, is added.
Adds parser unit tests (singular + count forms) and a discriminating
runtime test that casts Lictor and asserts the token is gated on an
opponent's entry (fails on revert). Removes Lictor from the parser
misparse backlog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw63XPowSxhpJaS9xM8Xyj
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe condition parser now handles opponent-controlled battlefield entries this turn, preserving opponent scope and thresholds. Unit and integration tests cover parsing and Lictor’s conditional token creation, while the misparse backlog removes Lictor from the affected category. ChangesLictor opponent entry condition
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant OracleParser
participant GameState
participant LictorTrigger
participant TokenCreation
OracleParser->>LictorTrigger: Parse opponent-scoped entry condition
GameState->>LictorTrigger: Provide battlefield entries this turn
LictorTrigger->>TokenCreation: Create Tyranid Warrior token when condition is met
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
matthewevans
left a comment
There was a problem hiding this comment.
Held on current head 3d767251. Manual implementation review finds the new opponent-scoped past-tense condition uses the existing per-opponent BattlefieldEntriesThisTurn authority and the integration tests cover both the trigger and its false branch. The parser/engine change still has no current-head <!-- coverage-parse-diff --> artifact because the card-data/Rust CI jobs are queued. That artifact is required evidence for this surface, so no approval or queue action yet.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/condition.rs`:
- Around line 12589-12614: Update
test_entered_this_turn_under_opponent_control_count to match aggregate:
AggregateFunction::Max within BattlefieldEntriesThisTurn, in addition to the
opponent player scope. Keep the existing comparator and threshold assertions
unchanged.
🪄 Autofix (Beta)
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: 6ea2206b-717a-432e-b8f3-c9b1d48dfbb6
📒 Files selected for processing (4)
crates/engine/src/parser/oracle_nom/condition.rscrates/engine/tests/integration/lictor_opponent_entered_this_turn.rscrates/engine/tests/integration/main.rsdocs/parser-misparse-backlog.md
Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main
|
|
Right seam, correct scope carrier, and a genuinely discriminating runtime pair — one fixup-sized test-adequacy gap (the Reviewed at head ✅ CleanCard premise is real. Scryfall
The PR body quotes this exactly. The apostrophe in the printed text is U+0027 straight (the only non-ASCII codepoint in the Oracle text is the U+2014 em-dash in the ability-word prefix), so the straight-apostrophe-only anchor at Bug premise is confirmed against generated card data, not taken on trust. In today's Right seam. Ordering is safe. The earlier Scope carrier is the correct one, and the reasoning in the doc comment is accurate. Verified against the runtime at All five CR citations grep-verify against
The negative test is not vacuous.
Blast radius measured directly, and it matches the claim. Across the full 35,477-card pool in Backlog bookkeeping is consistent — 🟡 Non-blocking[MED] The three shape assertions wildcard the very [LOW] [LOW] Parse-diff baseline is still pending. The Recommendation: push one fixup commit tightening the three Specific credit: routing the new surface through |
The three shape assertions matched `PlayerScope::Opponent { .. }`, which
also admits `AggregateFunction::Sum` — precisely the cross-opponent
summation the combinator's doc comment identifies as wrong and that is
only observable at three or more seats. Every test the PR adds would
still have passed if the parser were later changed to emit `Sum`.
Tighten all three sites to `{ aggregate: AggregateFunction::Max }` so the
existential "an opponent" reading is actually pinned by the test suite.
Co-authored-by: keloide <75585494+keloide@users.noreply.github.com>
matthewevans
left a comment
There was a problem hiding this comment.
Approving at head 03e8046914a7b1689654e8bdef0563fa0e647d47.
Right seam and idiomatic at it: the new parse_entered_this_turn_under_opponent_control combinator routes through the existing parse_or_more_entered_count / parse_entered_this_turn_subject helpers rather than standing up a fresh recognizer, and carries the opponent scope on PlayerScope::Opponent { aggregate: Max } — the existential "an opponent" reading already established on parse_opponent_had_entered_this_turn — instead of smuggling it into the TargetFilter. No new engine variant. The QUANTITY-path opponent surface is deliberately left at an honest Effect::Unimplemented because no printed card needs it, which is the correct call over speculative surface.
Test discrimination verified: the runtime pair drives the real cast pipeline and gates the Tyranid Warrior token on an opponent's entry, with a paired positive reach-guard so the negative assertion cannot pass vacuously.
CodeRabbit's single actionable finding (pin AggregateFunction::Max in the shape assertions) was filed against 3d767251 and is already resolved at this head by 03e80469 — confirmed against the head, not the review timestamp: both test_entered_this_turn_under_opponent_control_singular and _count now pin aggregate: AggregateFunction::Max, closing the wildcard that also admitted Sum (wrong cross-opponent summation, observable only at 3+ seats).
All required checks green on this head.
|
Temporarily dequeued by a maintainer — nothing is wrong with this PR, and it will be re-enqueued shortly.
That job is a The fix is #6568. It was queued behind the PRs it needs to unblock, which is a deadlock: the queue merges in order, so it could not land until those cleared, and they could not clear until it landed. Dequeuing the entries ahead of it is how that gets broken. No action needed from you. Your PR keeps its approval and its head is unchanged. Once #6568 merges, this will be re-enqueued and should go through on the same green it already had. Apologies for the churn — this was our breakage, not yours. |
Resolves the deterministic crates/engine/tests/integration/main.rs mod-line conflict by keeping both test modules in rustfmt-sorted order. Maintainer-side port: main advanced under this PR tonight (phase-rs#6538 phase-rs#6543 phase-rs#6545 phase-rs#6554 phase-rs#6563 phase-rs#6579). The phase-tauri 0.35.1->0.35.2 client/src-tauri/Cargo.lock bump is now a no-op, since main landed the identical bump in phase-rs#6568.
…rs#6554) * Fix Lictor opponent-scoped "entered this turn" intervening-if Lictor's Pheromone Trail — "When this creature enters, if a creature entered the battlefield under an opponent's control this turn, create a 3/3 green Tyranid Warrior creature token with trample" — dropped its intervening-"if" condition (parsed to None), so the ETB trigger fired unconditionally and the token was created every time Lictor entered. The "under your control" surface of this class was already supported; this adds the opponent-scoped, past-tense mirror. A new `parse_entered_this_turn_under_opponent_control` combinator reuses the shared `parse_or_more_entered_count` / `parse_entered_this_turn_subject` helpers and carries the scope via `PlayerScope::Opponent { Max }` — the existential "an opponent" reading already documented on `parse_opponent_had_entered_this_turn` — over the CR 608.2i `BattlefieldEntriesThisTurn` snapshot. No new engine variant. The opponent surface in the QUANTITY path stays honestly Unimplemented (no printed card); only the condition path, which Lictor uses, is added. Adds parser unit tests (singular + count forms) and a discriminating runtime test that casts Lictor and asserts the token is gated on an opponent's entry (fails on revert). Removes Lictor from the parser misparse backlog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gw63XPowSxhpJaS9xM8Xyj * test(PR-6554): pin opponent aggregate to Max in Lictor shape assertions The three shape assertions matched `PlayerScope::Opponent { .. }`, which also admits `AggregateFunction::Sum` — precisely the cross-opponent summation the combinator's doc comment identifies as wrong and that is only observable at three or more seats. Every test the PR adds would still have passed if the parser were later changed to emit `Sum`. Tighten all three sites to `{ aggregate: AggregateFunction::Max }` so the existential "an opponent" reading is actually pinned by the test suite. Co-authored-by: keloide <75585494+keloide@users.noreply.github.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
Lictor's Pheromone Trail — "When this creature enters, if a creature entered the battlefield under an opponent's control this turn, create a 3/3 green Tyranid Warrior creature token with trample" — dropped its intervening-"if" condition (parsed to None), so the ETB trigger fired unconditionally and the token was created every time Lictor entered.
The "under your control" surface of this class was already supported; this adds the opponent-scoped, past-tense mirror. A new
parse_entered_this_turn_under_opponent_controlcombinator reuses the sharedparse_or_more_entered_count/parse_entered_this_turn_subjecthelpers and carries the scope viaPlayerScope::Opponent { Max }— the existential "an opponent" reading already documented onparse_opponent_had_entered_this_turn— over the CR 608.2iBattlefieldEntriesThisTurnsnapshot. No new engine variant.The opponent surface in the QUANTITY path stays honestly Unimplemented (no printed card); only the condition path, which Lictor uses, is added.
Adds parser unit tests (singular + count forms) and a discriminating runtime test that casts Lictor and asserts the token is gated on an opponent's entry (fails on revert). Removes Lictor from the parser misparse backlog.
Claude-Session: https://claude.ai/code/session_01Gw63XPowSxhpJaS9xM8Xyj
Summary
Implementation method (required)
Method: /engine-implementer
Note
Any change to
crates/engine/game logic — parser, effects, resolver,targeting, rules behavior — is expected to go through
/engine-implementer.The "not used" box is for changes that genuinely fall outside that scope.
CR references
Verification
Required checks ran clean, or the exact CI-owned alternative is stated below.
Gate A output below is for the current committed head.
Final review-impl below is clean for the current committed head.
Both anchors cite existing analogous code at the same seam.
<exact command or CI check>—Gate A
Gate A PASS head=<40-hex-sha> base=<40-hex-sha>
Anchored on
Final review-impl
Final review-impl PASS head=<40-hex-sha>
Claimed parse impact
Summary by CodeRabbit