Skip to content

Engine: the tracked-set publish arm for head TargetOnly is unshipped β€” 82 gated rows, the largest head (Trial of Agony, Deliver Unto Evil, The Doctor's Childhood Barn)Β #7464

Description

@lgray

πŸ€– AI text below πŸ€–

Related: #6857

Summary

#6857's root cause is that affected_objects_from_events
(crates/engine/src/game/effects/mod.rs:5590) has no arm for effect heads that affect objects without
emitting a per-object event: they fall through to the _ => ZoneChanged harvest, publish an
empty tracked set, and every downstream TargetFilter::TrackedSet anaphor binds nothing.

The #6857 PR contracted, by maintainer ruling, to a measured core β€” only heads where every
firing row is dispositioned by runtime measurement ship (PumpAll, GoadAll, GiveControl).
TargetOnly is the largest excluded head and is carried here. This is scope, not a deferral.

Numbers (corrected census, BASE 9b7c66e30)

82 production-gated TargetOnly rows. No structural partition of that 82 into
"would publish" / "structurally excluded" was computed for this head β€” the census round that computed
that partition covered GoadAll, GiveControl, CopySpell, PumpAll and Pump only. So the count
of rows that would actually publish is unknown, which is itself part of the burden.

What is known: an earlier round measured 13 previously-unnamed TargetOnly rows at runtime β€”
1 FIX (march of progress), 5 PRESERVED (archfiend of depravity, disorienting choice, lucius the
eternal, run away together, time to feed), 5 REGRESSION (cryptek, fatal fissure, reincarnation,
rushed rebirth, together forever β€” all removed by the delayed-trigger exclusion), and 2
behaviour-change rows detailed below. That is 13 of 82.

Three rows worth recording individually

trial of agony β€” a caster-advantageous wrong-player prompt (root cause is #7460)

Card: "Choose two target creatures controlled by the same opponent. That player chooses one of
those creatures. Trial of Agony deals 5 damage to that creature, and the other can't block this turn."

Parsed chain: TargetOnly{Creature} -> ChooseFromZone{1, Exile, chooser: Controller} -> DealDamage{5, TriggeringSource}.

HEAD with the publish
prompt none (Priority) ChooseFromZoneChoice
addressee (measured) β€” CONTROLLER (PlayerId(0)) β€” the caster
prompt contents β€” cards=[ObjectId(1), ObjectId(2)] count=1 up_to=false
damage before the answer Alpha 5, Beta 5 Alpha 0, Beta 0
damage after answering "Alpha" (n/a) Alpha 5, Beta 0
"the other can't block" absent absent

Two separable findings:

Also relevant: the pool this prompt offers is subject to the ChooseFromZone chain-set pre-emption
defect filed alongside this issue.

deliver unto evil β€” prompt correct, outcome moot

Card: "… Otherwise, an opponent chooses two of them. Leave the chosen cards in your graveyard and
put the rest into your hand."

Addressee measured CORRECT: OPPONENT (PlayerId(1)), count=2, both cards offered β€” matching the
card. The prompt is the only observable: both the if and the else branch terminate in
Effect::Unimplemented{"Leave the chosen cards in your graveyard and put the rest into your hand"}, so
there is no post-choice outcome to be right or wrong about. Net: HEAD silently skips a prompt the card
requires; the publish surfaces the correct prompt in front of a dead tail.

This row and trial of agony have the same surface shape (Priority -> ChooseFromZoneChoice with a
populated pool) and opposite addressees β€” the discriminator is the parsed chooser field, which is
exactly #7460's subject.

the doctor's childhood barn β€” measured UNREACHABLE; the card stays broken

Census row: trigger[0], head TargetOnly, repeat_for=true, later_pub=true. The
SetTapState node below it is itself in publisher position β€” its own sub-chain is
PhaseOut -> GenericEffect{ParentTarget, CantPhaseIn}, which consumes β€” so leg 2 of the publish
guard declines and the new TargetOnly arm never runs.

HEAD       sets=[(1, [1])]  #1 Theirs  phased_in=false  tap=TRUE   TCE CantPhaseIn on #1
CANDIDATE  sets=[(1, [1])]  #1 Theirs  phased_in=false  tap=TRUE   TCE CantPhaseIn on #1   <- byte-identical

The mechanism is leg 2 β€” explicitly NOT the empty-ability.targets repeat_for fan-out story
proposed in an earlier review round (the shellshock shape). ability.targets here is populated.
Discriminating pair, measured: the same fixture with the "can't phase in" clause removed clears leg 2,
and the candidate then does untap the permanent (tap=true -> tap=false). So the publish mechanism
works and leg 2 is precisely what blocks this card.

What a future PR needs

  1. A structural partition of the 82 rows (leg-2 declines + delayed-trigger exclusion) so the firing
    count is known at all.
  2. Runtime disposition of the firing remainder beyond the 13 already measured.
  3. The ChooseFromZone pool defect resolved first for the rows whose consumer is a ChooseFromZone
    (trial of agony, deliver unto evil) β€” otherwise a publish arm converts "silently wrong" into
    "wrongly prompted".

Evidence provenance

Row count from a census whose predicate is production's own next_sub_needs_tracked_set (called, not
mirrored) over the full card export at BASE 9b7c66e30. Runtime rows measured one-binary, env-toggled,
with a known-changed control and a no-consumer negative control in each run; prompt addressees resolved
explicitly via WaitingFor::acting_player(). The 69 rows not enumerated above are unmeasured and no
runtime symptom is asserted for them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions