fix: use per-attacker attack target legality#4936
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
matthewevans
left a comment
There was a problem hiding this comment.
Thanks. I did a backend pass; frontend/client acceptance remains deferred under current policy, and this PR is also currently conflicting with base.
Backend blockers:
-
Goad still allows non-player targets when a non-goading player is attackable. CR 701.15b requires the goaded creature to attack a player other than the goader if able; treating a planeswalker or battle target by its defending player does not satisfy that direct player-attack requirement. Please keep goad-satisfying targets to direct
AttackTarget::Player(pid)targets when such a non-goading player is available. -
Phase AI still chooses from the global
valid_attack_targetslist rather than the newvalid_attack_targets_by_attackermap. A scoped attacker can be assigned a target that is globally legal but illegal for that attacker, and the validation fallback can then choose the empty declaration. Please thread the per-attacker target map through the combat chooser or select from engine-filtered legal declare-attackers candidates.
c217dc1 to
c8faa0a
Compare
|
Backend re-review on
I’m not approving or enqueueing this from the sweep: this is still a mixed backend/frontend PR under the FE-defer policy (label already applied), and GitHub currently reports conflicts with base ( |
…ue-4765-per-attacker-attack-targets # Conflicts: # client/src/components/controls/AttackTargetPicker.tsx # client/src/utils/__tests__/combat.test.ts # client/src/utils/combat.ts # crates/phase-ai/src/combat_ai.rs # crates/phase-ai/src/search.rs
matthewevans
left a comment
There was a problem hiding this comment.
Backend re-review on 94277615ad after the merge-from-main update. The earlier goad/per-attacker legality issues still look conceptually addressed, but the current head has a backend/AI blocker:
[MED] The new AI entrypoint now fails the Rust lint gate. CI is failing Rust lint (fmt, clippy, parser gate) with:
error: this function has too many arguments (8/7)
--> crates/phase-ai/src/combat_ai.rs:155:1
pub fn choose_attackers_with_targets_with_profile(...)
The new valid_attack_targets_by_attacker parameter is the right data to thread, but this public-ish AI helper has crossed the clippy limit. Please fold the attack-context inputs into a small parameter struct or otherwise reuse the existing declaration context shape so the per-attacker map does not just proliferate the function signature.
Frontend/client acceptance remains deferred to Matt under the current sweep policy; I also see the current frontend gate red in AttackTargetPicker.test.tsx, but I’m not treating frontend acceptance from this sweep.
matthewevans
left a comment
There was a problem hiding this comment.
Backend re-review on current head 104bd11d: the prior backend clippy blocker from 94277615 appears addressed and Rust lint is green.
I’m still not approving or enqueueing this from the sweep because this remains a mixed backend/frontend PR with frontend/client acceptance deferred under current policy (defer-fe stays), and the current GitHub check set still has failing Rust test shards plus the frontend gate. This needs either a clean current-head test state and Matt FE acceptance, or another author update if those failures are from the PR.
…split fill order Two test regressions from the per-attacker target map threading: 1. choose_attackers_with_targets fetched engine target lists that are scoped to state.active_player, producing empty candidates when the decision is for a different player (multi-player bot tests). Only thread the engine lists when player == state.active_player; otherwise fall back to can_attack + players::opponents. 2. legal_attack_targets_for_attacker returned None when an attacker was absent from the per-attacker map, collapsing every attacker to no legal targets in scenarios that supply an empty/stale map. Fall back to valid_attack_targets instead, matching the engine's own candidate generator (attacker_actions). 3. Frontend spreadStackEvenly used a greedy least-loaded round-robin that assigned ascending-id members to alternating targets instead of filling targets in display order. Rewrite it to use the shared evenSplit building block for per-target capacities, then fill in display order with ascending-id members.
Parse changes introduced by this PR✓ No card-parse changes detected. |
matthewevans
left a comment
There was a problem hiding this comment.
Backend re-review on 892f97f7 found the new AI target-scoping change itself aligned with the engine's per-attacker target handling, but the PR is still not merge-ready.
CI is red in Rust tests (shard 2/2) on engine::game::combat::tests::goad_still_forces_redirect_when_an_attackable_non_goading_player_exists. The failing assertion is the PR's goad redirect regression test: declare_attackers now returns an error string containing must attack a different player directly if able, while the test asserts the older substring must attack a different player if able. Please make the diagnostic/test agree so the discriminating backend test passes.
Also noting the existing sweep policy state: this PR still carries defer-fe, so client/UI acceptance remains deferred to Matt even after the backend test is fixed.
The goad redirect error at combat.rs:2524 reads 'must attack a different player directly if able (CR 701.15b)', but the test still asserted the older 'must attack a different player if able' substring (missing 'directly'). The 'directly' wording is the more precise one — it matches CR 701.15b's requirement for a direct player attack, not a planeswalker/battle proxy — so update the test assertion to match.
|
Backend re-review on current head I’m still not approving or enqueueing this from the sweep because this remains a mixed backend/frontend PR under the FE-defer policy ( |
|
hi, @matthewevans, could you review my PR, plz? |
# Conflicts: # crates/engine/src/game/combat.rs
|
Holding current head |
…ing_gate fixture New test file from main merge constructs WaitingFor::DeclareAttackers without the per-attacker target field this PR adds to the variant.
|
Current-head backend/mixed re-review on I’m still not approving or enqueueing this from the sweep because this remains a mixed backend/frontend PR with |
…ttacker_restriction into attacker_is_generically_eligible_to_attack
|
Processed current head I am still not approving or enqueueing this PR because it remains a mixed backend plus frontend/client PR with |
|
hi, @matthewevans, can your review my PR ,plz? |
matthewevans
left a comment
There was a problem hiding this comment.
Frontend review on current head e42ea9686486dce606a366e9fe129b2e559091f9:
I agree this PR is solving a real bug class. The old frontend had only a global valid_attack_targets list and could default every selected attacker to a target that was legal for the step but illegal for that specific attacker. The new engine-authored valid_attack_targets_by_attacker payload is the right authority to thread through the client, and the core direction looks mergeable once the remaining frontend polish is addressed.
Please make these frontend follow-ups before merge:
-
Restore the useful rationale comments removed from
AttackTargetPicker.tsx, updated for the new per-attacker legality behavior. The comments around the PeekTab/scroll-container layout and the assignment transform semantics were explaining non-obvious constraints; removing them makes this component harder to safely maintain. -
Add component-level coverage for mixed per-attacker target legality. The current new tests cover
client/src/utils/combat.ts, butAttackTargetPicker.test.tsxstill renders withoutvalidTargetsByAttacker, so the picker UI path for common-target filtering, disabled illegal steppers, legality-aware spread, and final Distribute confirmation is not directly exercised. -
Handle the empty common-target default mode.
selectedAttackersNeedTargetPickercan open the picker when at least one attacker has multiple legal targets, butAttackTargetPickerstarts in Attack All mode and renders onlycommonTargets. If selected attackers have no shared target, the dialog opens with no target buttons until the user manually switches to Distribute. Defaulting to Distribute whencommonTargets.length === 0, or showing a translated empty-state affordance, would close that UX gap.
Once those are addressed, I think the frontend side is acceptable and this should be mergeable from the FE perspective, assuming the current green checks stay green.
# Conflicts: # client/src/utils/__tests__/combat.test.ts
|
Sweep note: still DEFER-FE on current head. I am not approving, enqueueing, or merging this from the automation sweep because the PR remains a mixed backend/frontend surface from a non-exempt author and frontend acceptance is reserved for Matt's direct handling under current policy. |
# Conflicts: # client/src/components/controls/AttackTargetPicker.tsx # crates/manabrew-compat/src/lib.rs
Summary
Fixes #4765 by teaching declare-attackers to use per-attacker legal target sets instead of a single global target list. This prevents the client and AI from submitting attacks that are globally attackable in the step but illegal for a specific attacker, which is what caused the reported
ObjectId(...) can't attack Player(...)failure.Implementation method (required)
How was the engine/parser logic in this PR produced? Check exactly one:
/engine-implementerpipeline (plan → review-plan → implement → review-impl → commit)/engine-implementer— explain why belowIf you did not use
/engine-implementer, state why (e.g. frontend-onlychange, docs/CI/tooling change, release chore, or a fix too small to warrant
the pipeline):
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
Touched/added references include:
CR 508.1aCR 508.1bCR 508.1cCR 508.1dCR 508.5CR 701.15bCR 702.26bCR 302.6Verification
gh issue view 4765 -R phase-rs/phase --json number,title,body,state,url,labelsclient/src/utils/__tests__/combat.test.tscrates/engine/src/game/combat.rscargo check -p engine --testscargo check -p engine --libcargo test -p engine valid_attack_targets_by_attacker_filters_scoped_attack_lockouts -- --exactCI=true pnpm test -- --run src/utils/__tests__/combat.test.ts