refactor(engine): route collect-evidence and unless-bounce cost moves through the zone pipeline#6041
Conversation
… through the zone pipeline Migrate the last two semantic-cost raw zone movers onto zone_pipeline::move_object with ZoneMoveRequest::cost: - collect_evidence::handle_choice (CR 701.59a): selected evidence cards exile one at a time through the replacement-aware pipeline; a replacement choice parks the new typed PendingCostMoveResume::CollectEvidencePayment root (program-counter resume over the unpaid suffix, completion exactly once). - handle_unless_bounce_choice (CR 118.12): the selected return-to-hand unless cost parks PendingCostMoveResume::UnlessBouncePayment; the paid cost keeps the avoided effect avoided across delivered, redirected, and prevented outcomes (CR 118.11). Both roots resume only through drain_pending_cost_move_resume at the delivered and prevented boundaries — the eligibility tables are extended, no dispatch-site sequential-ifs, no PostReplacementContinuation growth, no pending_continuation reuse. Witnesses: redirect-pause, prevented-boundary, and synchronous regression coverage for both roots in cost_zone_pipeline; exactly-once completion asserted on the CollectEvidence action event.
There was a problem hiding this comment.
Code Review
This pull request updates the engine's cost-payment pipeline to support replacement-aware moves for 'Collect Evidence' and 'Unless' bounce effects. It introduces new variants to PendingCostMoveResume and updates the engine's state machine to handle these costs through the zone_pipeline, ensuring that replacement effects are correctly processed. Integration tests were added to verify that these costs pause for and correctly resume after replacement events. The review comment correctly identifies a potential non-deterministic iteration issue when processing the chosen evidence cards and provides an idiomatic Rust fix using a seen set to maintain order.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // needs `Cause::Cost` with the source extracted from the resume — left for | ||
| // the cost-payment tranche. | ||
| super::super::zones::move_to_zone(state, id, Zone::Exile, events); | ||
| move_evidence_costs(state, player, chosen.to_vec(), 0, resume.clone(), events) |
There was a problem hiding this comment.
Non-deterministic iteration over HashSet can lead to divergent game states. When deduplicating items during iteration, use a seen membership set (such as a HashSet) inside the loop to filter duplicates rather than collecting the entire collection into a HashSet first, in order to preserve the deterministic iteration order of the original collection.
let mut seen = std::collections::HashSet::new();
let mut chosen_vec = Vec::new();
for item in chosen {
if seen.insert(item) {
chosen_vec.push(item);
}
}
move_evidence_costs(state, player, chosen_vec, 0, resume.clone(), events)References
- When deduplicating items during iteration, use a
seenmembership set (such as aHashSet) inside the loop to filter duplicates rather than collecting the entire collection into aHashSetfirst, in order to preserve the deterministic iteration order of the original collection.
Parse changes introduced by this PR✓ No card-parse changes detected. |
…-2 hits / -2 rows)
Migrate the last two semantic-cost raw zone movers onto
zone_pipeline::move_object with ZoneMoveRequest::cost:
exile one at a time through the replacement-aware pipeline; a
replacement choice parks the new typed
PendingCostMoveResume::CollectEvidencePayment root (program-counter
resume over the unpaid suffix, completion exactly once).
unless cost parks PendingCostMoveResume::UnlessBouncePayment; the paid
cost keeps the avoided effect avoided across delivered, redirected,
and prevented outcomes (CR 118.11).
Both roots resume only through drain_pending_cost_move_resume at the
delivered and prevented boundaries — the eligibility tables are
extended, no dispatch-site sequential-ifs, no PostReplacementContinuation
growth, no pending_continuation reuse.
Witnesses: redirect-pause, prevented-boundary, and synchronous
regression coverage for both roots in cost_zone_pipeline; exactly-once
completion asserted on the CollectEvidence action event.