Card(s)
Valakut Exploration (first witness). Class: any <mass zone move>, then <effect> that much/that many to each player/opponent chain.
Build/version
Current main (14b11cf5b), observed with the parse landed by the Valakut Exploration misparse fix (PR forthcoming, will be linked here).
Actual behavior
When a ChangeZoneAll completes and a chained sub-effect consumes Ref(EventContextAmount), the amount each recipient reads is not the total number of cards moved:
effects/mod.rs::previous_effect_counts_by_player_from_events (~7411) tallies the completed move's ZoneChanged events per owner;
install_previous_effect_counts_by_player (~7476) installs that per-player table and sets last_effect_count/last_effect_amount to the per-player max, not the total;
- the parent-effect postlude (~10105–10118) runs this before the sub resolves;
DamageEachPlayer (deal_damage.rs:~2268) resolves per-recipient via resolve_quantity_scoped_with_targets, whose EventContextAmount cascade (quantity.rs:~3686) consults the per-player table before the scalar channels — so each opponent reads its own swept-card count.
Concretely for Valakut Exploration ("put them into their owner's graveyard, then this enchantment deals that much damage to each opponent"): in the card's native pattern every swept card is the controller's own (impulse-exiled off their library), so each opponent's per-player count is 0 and the chained damage resolves for 0 — while coverage counts the card supported with no warning. A mixed-ownership pool (one P0-owned + one P1-owned card) resolves −1/0 — discriminating the per-player-table mechanism from both scalar candidates (stamped total 2 would give −2/−2; installed max would give −1/−1).
Expected behavior
"That much" is the total number of cards moved by the preceding clause (CR 608.2c — later text reads the earlier action's result; matches the card's official ruling). Two cards swept → each opponent is dealt 2. Note there is currently no QuantityRef that can encode the total after the install (the scalar channel itself holds the max), so this needs an engine-side channel rather than a parser workaround.
Steps to reproduce
crates/engine/tests/integration/valakut_exploration_end_step_exile_sweep.rs (landing in the linked PR) carries three CHARACTERIZATION assertions with explicit update-me markers — they are the acceptance criteria for this issue:
- T1 (mixed-ownership pool, 2 cards): currently −1/0 → should be −2/−2
- T2 (native landfall pattern, 1 controller-owned card): currently 0 → should be −1
- T4 (foreign-link hostile, 1 linked card): currently 0 → should be −1
The sweep/gate/link-tracking semantics around them are fully proven (revert-discriminating); only the amount channel is characterized.
Additional context
Found by a downstream consumer's structural cross-check while fixing the card's parser-side misparse (existential intervening-if + swallowed conjunct). The parse now emits the proven DamageEachPlayer{Ref(EventContextAmount), Opponent} shape (Shadowheart's), which is target-state-correct — the gap is exclusively in how the amount channel aggregates after a mass move.
Card(s)
Valakut Exploration (first witness). Class: any
<mass zone move>, then <effect> that much/that many to each player/opponentchain.Build/version
Current main (
14b11cf5b), observed with the parse landed by the Valakut Exploration misparse fix (PR forthcoming, will be linked here).Actual behavior
When a
ChangeZoneAllcompletes and a chained sub-effect consumesRef(EventContextAmount), the amount each recipient reads is not the total number of cards moved:effects/mod.rs::previous_effect_counts_by_player_from_events(~7411) tallies the completed move'sZoneChangedevents per owner;install_previous_effect_counts_by_player(~7476) installs that per-player table and setslast_effect_count/last_effect_amountto the per-player max, not the total;DamageEachPlayer(deal_damage.rs:~2268) resolves per-recipient viaresolve_quantity_scoped_with_targets, whoseEventContextAmountcascade (quantity.rs:~3686) consults the per-player table before the scalar channels — so each opponent reads its own swept-card count.Concretely for Valakut Exploration ("put them into their owner's graveyard, then this enchantment deals that much damage to each opponent"): in the card's native pattern every swept card is the controller's own (impulse-exiled off their library), so each opponent's per-player count is 0 and the chained damage resolves for 0 — while coverage counts the card supported with no warning. A mixed-ownership pool (one P0-owned + one P1-owned card) resolves −1/0 — discriminating the per-player-table mechanism from both scalar candidates (stamped total 2 would give −2/−2; installed max would give −1/−1).
Expected behavior
"That much" is the total number of cards moved by the preceding clause (CR 608.2c — later text reads the earlier action's result; matches the card's official ruling). Two cards swept → each opponent is dealt 2. Note there is currently no
QuantityRefthat can encode the total after the install (the scalar channel itself holds the max), so this needs an engine-side channel rather than a parser workaround.Steps to reproduce
crates/engine/tests/integration/valakut_exploration_end_step_exile_sweep.rs(landing in the linked PR) carries three CHARACTERIZATION assertions with explicit update-me markers — they are the acceptance criteria for this issue:The sweep/gate/link-tracking semantics around them are fully proven (revert-discriminating); only the amount channel is characterized.
Additional context
Found by a downstream consumer's structural cross-check while fixing the card's parser-side misparse (existential intervening-if + swallowed conjunct). The parse now emits the proven
DamageEachPlayer{Ref(EventContextAmount), Opponent}shape (Shadowheart's), which is target-state-correct — the gap is exclusively in how the amount channel aggregates after a mass move.