Skip to content

feat(analysis): combo-detection PR-3 — detect mandatory-loop winner for drain-cascade combos (CR 704.5a) - #4480

Merged
matthewevans merged 2 commits into
phase-rs:mainfrom
lgray:ship/combo-detect-pr3
Jun 27, 2026
Merged

feat(analysis): combo-detection PR-3 — detect mandatory-loop winner for drain-cascade combos (CR 704.5a)#4480
matthewevans merged 2 commits into
phase-rs:mainfrom
lgray:ship/combo-detect-pr3

Conversation

@lgray

@lgray lgray commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Combo-detector series

This is PR-3 of the staged, offline-first infinite-combo / loop detector (Engine A). The series goal is to incrementally build combo/loop detection toward shortcutting mandatory, game-ending loops — recognizing that a repeating modulo game state with no meaningful out must resolve to a deterministic winner, rather than grinding hundreds of beats to the CR 704.5a life-loss death.

Pos PR State Delivers
PR-0 #4092 MERGED ResourceVector + loop_states_equal_modulo_resources (modulo-resource loop equality / reuse fingerprint); additive, no behavior change.
PR-1 #4097 MERGED Analysis sim harness around GameRunner::act that feeds/consumes ResourceVector.
PR-2 #4119 MERGED Net-progress detector (analysis/loop_check.rs detect_loopLoopCertificate) + corpus harness driving Priest+Umbral & Marwyn+Sword loop certificates.
PR-3 this PR open A live mandatory-loop winner shortcut (reconcile_terminal_result + live_mandatory_loop_winner): detects a repeating modulo state and emits GameOver immediately, ahead of the existing emit_resolution_halt runaway backstop.

Predecessors:

(Foundation: PR-0#4092#4092ResourceVector + modulo-resource loop equality.)

Summary

Adds a live mandatory-loop winner shortcut for drain-cascade combos. When priority passes in a cycle with a non-empty stack and the loop-detect ring shows a repeating modulo game state, reconcile_terminal_result identifies the single non-falling living player and emits GameOver{winner} immediately — instead of grinding hundreds of beats to the CR 704.5a life-loss death.

What it does

  • Detection (§2/§3): ring accumulation in pass_priority_once_with_pipeline + a modulo-match shortcut in reconcile_terminal_result. loop_detect_ring is #[serde(skip)] + eq-excluded (zero wire surface); reuses the existing GameOver event (no new variant, no inventory regen).
  • Soundness firewalls (§8/§9): no_living_player_has_meaningful_priority_action probes all living players (not just the current holder); live_mandatory_loop_winner returns None for mutual-drain / can't-lose / net-zero / multi-faller boards. A player with a meaningful out is never shortcut (CR 104).
  • Defensive probe guard: a thread-local guard keeps legality probes from running game-ending shortcut logic. Composes with perf(engine): eliminate O(N^2) mana sweep in legality probes on go-wide boards #4479's apply_as_current_for_legality perf path (perf'd legality apply run under the guard).
  • Corpus promotions: Sanguine Bond + Exquisite Blood (idx 17, "target opponent loses that much life" — auto-resolves to the sole legal target) and Marauding Blight-Priest + Bloodthirsty Conqueror (idx 18) promoted to DRIVEN_ROW_INDICES, each backed by a non-vacuous live apply(PassPriority) test observing GameOver at beat 6.

Verification

  • cargo build -p engine clean; cargo clippy -p engine --lib --tests -- -D warnings clean.
  • Live hard-gate tests pass: drive_drain_idx18_wins_live, drive_drain_idx17_targeted_wins_live, bounded-termination, victim-with-out-not-eliminated.
  • Full analysis:: suite 96/0. Non-vacuity measured by source revert (disabling the §2 ring sample or the §3 shortcut drops the early GameOver).
  • All CR annotations grep-verified against the Comprehensive Rules.

Rebased onto current main; the single conflict (with #4479's legality-probe perf refactor) was resolved by composing both changes and re-verified green.

… (CR 704.5a)

When priority passes in a cycle with a non-empty stack and the loop-detect ring shows a repeating modulo game state, reconcile_terminal_result identifies the single non-falling living player and emits GameOver{winner} immediately, instead of grinding hundreds of beats to the CR 704.5a life-loss death.

All-living-player meaningful-action firewalls (CR 104) ensure a player with a meaningful out is never shortcut, and live_mandatory_loop_winner returns None for mutual-drain / can't-lose / net-zero / multi-faller boards.

Promotes Sanguine Bond + Exquisite Blood (idx 17, targeted) and Marauding Blight-Priest + Bloodthirsty Conqueror (idx 18) to DRIVEN_ROW_INDICES, each backed by a non-vacuous live apply(PassPriority) test observing GameOver at beat 6. loop_detect_ring is serde-skipped + eq-excluded (zero wire surface); reuses the existing GameOver event (no new variant). A thread-local probe guard keeps legality probes from running game-ending shortcut logic.

Assisted-by: ClaudeCode:claude-opus-4.8
@lgray
lgray requested a review from matthewevans as a code owner June 27, 2026 16:49

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements live loop-shortcut detection (PR-3 Option C) for mandatory net-progress drain cascades. It introduces a persisted loop_detect_ring on GameState to track post-resolution snapshots across beats, canonicalizes volatile stack entry IDs in project_out_resources to enable modulo-equality matching, and couples the loop classifier into the live reducer via live_mandatory_loop_winner. It also adds defensive guards (SimulationProbeGuard) to prevent shortcutting during AI legality probes and ensures all living players are checked for loop-ending actions. The review feedback identifies a performance improvement opportunity in reconcile_terminal_result to avoid unnecessary heap allocation and Arc cloning when iterating over the loop detection ring.

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.

Comment on lines +299 to +310
// Clone the Arc handles (cheap refcount bumps) to release the borrow on the
// ring before the GameOver mutation below.
let priors: Vec<std::sync::Arc<GameState>> =
state.loop_detect_ring.iter().cloned().collect();
let cur = crate::analysis::resource::ResourceVector::snapshot(state);
if let Some(winner) = priors.iter().find_map(|prior| {
let delta = crate::analysis::resource::ResourceVector::delta(
&crate::analysis::resource::ResourceVector::snapshot(prior),
&cur,
);
crate::analysis::loop_check::live_mandatory_loop_winner(prior, state, &delta)
}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[MEDIUM] Unnecessary Heap Allocation and Arc Cloning in Hot Path

Cloning the entire loop_detect_ring into a Vec to release the borrow on state is unnecessary. Since PlayerId is a Copy type and does not borrow state, the borrow of state by find_map is completely released as soon as find_map returns.

By iterating over state.loop_detect_ring directly and storing the result of find_map in a local variable, we can avoid allocating a Vec on the heap and performing up to 16 Arc clones (refcount increments/decrements) on every priority pass check when the stack is active.

        let cur = crate::analysis::resource::ResourceVector::snapshot(state);
        let winner = state.loop_detect_ring.iter().find_map(|prior| {
            let delta = crate::analysis::resource::ResourceVector::delta(
                &crate::analysis::resource::ResourceVector::snapshot(prior),
                &cur,
            );
            crate::analysis::loop_check::live_mandatory_loop_winner(prior, state, &delta)
        });
        if let Some(winner) = winner {

@matthewevans

Copy link
Copy Markdown
Member

Current-head maintainer review found no content blocker on the net GitHub API diff. I reviewed the reducer/analysis/GameState/SBA surfaces for the live drain-cascade shortcut: the shortcut is at the terminal reconciliation seam, samples are captured after post-action trigger placement, the winner classifier reuses the existing modulo-resource detector plus SBA can't-lose/can't-win predicates, and the production-path tests cover both live drain loops plus a victim-with-a-meaningful-out negative.

I ran update-branch because the PR was behind; GitHub produced merge-only head 2ecacbfe1192d9436bd27e276dfa3b4c617b713c. Holding approval/labels/enqueue until the fresh exact-head CI on that head completes.

@lgray lgray changed the title feat(analysis): detect mandatory-loop winner for drain-cascade combos (CR 704.5a) feat(analysis): combo-detection PR-3 — detect mandatory-loop winner for drain-cascade combos (CR 704.5a) Jun 27, 2026
@matthewevans matthewevans self-assigned this Jun 27, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved current head 2ecacbfe1192d9436bd27e276dfa3b4c617b713c after the earlier Tier 2 review and fresh exact-head CI.

Review basis: GitHub API diff, isolated worktree, local CR lookup for the cited 732.2a/732.5/704.5a/603.3/101.2/104.x rules, Gemini context, and the review-impl reducer/analysis/GameState/SBA lenses. No content blocker found: the live shortcut stays at terminal reconciliation, loop samples are captured after post-action trigger placement, the winner path reuses the modulo-resource detector and existing SBA can-lose/can-win predicates, and the production-path tests cover both live drain positives plus the victim-with-meaningful-out negative.

Exact-head CI is green/CLEAN, and the repush guard held after assignment reread.

@matthewevans matthewevans added feature Larger-scoped feature status:ready-to-merge Maintainer-reviewed and ready to merge labels Jun 27, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jun 27, 2026
Merged via the queue into phase-rs:main with commit c1b61de Jun 27, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Larger-scoped feature status:ready-to-merge Maintainer-reviewed and ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants