Monte Carlo Multi-Way Equity Engine — Implementation Summary

# Monte Carlo Multi-Way Equity Engine

## Implementation

### Hybrid approach
- **Heads-up (1 opponent):** Exhaustive enumeration via `compute_vs_range` (exact, trie-optimized)
- **Multi-way (2+ opponents):** Monte Carlo via `compute_vs_range_multiway_shared` (true multi-way equity)

### Key methods

#### `HandPotential::compute_vs_range_multiway_shared`
Production path — all opponents share a single range. Avoids cloning the 2704-entry HandRange once per opponent.

```rust
pub fn compute_vs_range_multiway_shared(
    &self,
    hole: &[Card; 2],
    board_cards: &[Card],
    range: &HandRange,
    num_opponents: usize,
    num_samples: usize,
    seed: u64,
) -> PotentialResult
```

#### `HandPotential::compute_vs_range_multiway`
Original per-opponent-range version. Still used by tests. Both delegate to shared `run_multiway_mc` core via `prepare_mc_deck` helper.

#### `HandPotential::run_multiway_mc` (private core)
Shared MC loop used by both public methods. Takes pre-built sparse pair references.

### MC Sampling Algorithm (optimized in round 2)

For each sample:
1. Reset `present[]` mask — O(n) boolean array (replaces old O(n) card copy + swap_remove)
2. Deal N opponent hands via `sample_sparse` — iterates precomputed sparse pair list O(k) instead of O(n²)
3. Each opponent: filter by `present[i] && present[j]`, sample proportional to weight
4. Mark dealt cards as `present[i] = false`
5. Accumulate importance weight = product of normalization constants
6. Rebuild `available` from present cards, shuffle, deal completion cards
7. Evaluate hero vs **strongest opponent** (true multi-way equity)
8. Classify HP transition (before → after) against strongest opp
9. Accumulate weighted HP matrix

### Precomputation (once per equity call, shared across all MC samples)
- `nonzero_pairs_by_cards(cards)` → `Vec<(usize, usize, f64)>` — sparse list of in-range card-index pairs
- For shared range: computed once, references repeated N times

### Performance
| Scenario | Evaluations | Time |
|----------|------------|------|
| 2 opp, M=20K | ~200K | ~20ms |
| 5 opp, M=20K | ~300K | ~30ms |
| 9 opp, M=20K | ~380K | ~38ms |
| 9 opp, M=100K | ~1.9M | ~190ms |

MC is faster than exhaustive even for HU (0.3ms vs 5ms), but we keep exhaustive for HU because it's exact.

### Why not individual rollouts?
Product formula (hs₁ × hs₂ × ... × hsₙ) underestimates by 15-25pp due to positive correlation:
- Top pair (hs=0.7 each): product = 0.34³ = 3.9% vs true MC ~28% for 9 opponents
- ppot/npot don't extend to multi-way at all

### Round 2 Optimizations

| Change | Old | New | Benefit |
|--------|-----|-----|---------|
| MC sampling | `sample_weighted` rescans O(n²) per sample | `sample_sparse` over precomputed O(k) list | ~10-50× faster per sample |
| Card removal | `swap_remove` on Vec (O(n) + index juggling) | `present[]` boolean mask (O(1) mark) | Simpler, no index bugs |
| Strong hands sort | `sort_unstable_by` (O(n log n)) | `select_nth_unstable_by` (O(n) partial sort) | Only need top-k, not full sort |
| OESD detection | Heap `Vec<u8>` allocation per pair | Fixed `[u8; 6]` stack array | Zero allocation |
| Fallback result | Duplicated struct literals (3 sites) | `PotentialResult::fallback()` | DRY |
| Board completion | Triplicated match block | `board_completion_count` const fn | DRY |
| Guard+setup | Duplicated in both methods | `prepare_mc_deck` shared helper | DRY, prevents drift |
| Union | Separate 52×52 loop | Delegates to `weighted_union(other, 1.0)` | DRY |

### Test coverage
- 24 MC tests in hand_potential.rs (matches exhaustive, multi-way < HU, river zero potential, empty ranges, deterministic, 9 opponents, conditional sampling)
- 13 strategy tests in rollout_postflop_gen3.rs
- Total: 340 tests pass, clean build, 0 warnings

### Simulation results (10K hands, 6-max, seed 42)
| Bot | Avg BB/Hand |
|-----|-------------|
| Gen3-2 | +3.31 |
| Gen2-2 | +2.06 |
| Gen3-1 | +1.75 |
| Gen2-1 | -0.02 |
| Gen1-2 | -2.66 |
| Gen1-1 | -4.44 |

Gen3 outperforms Gen2 by ~1.5 BB/hand in 6-max. However, in 10-seat 5v5 format, Gen3 currently underperforms Gen2 (negative delta) — this is being investigated via round 2 sweep.

## Next Steps
1. **Round 2 sweep analysis** — 16 params × 3 values × 4 seeds = 192 runs (running)
2. **RangePredictor** — per-opponent range tracking from observed actions
3. **Gen 2 MC migration** — Gen 2 could use MC for multi-way (10-50× speedup)
4. **Port missing tactics** — backAlleyMug, luringBet, scareCardHit
5. **Board texture awareness** — connected/monotone/paired detection

id: 29259ee3718a4bada4de157ef58cec3d
parent_id: 1246bbc3bb4948fc8329079b84b4ae3d
created_time: 2026-06-12T18:08:16.480Z
updated_time: 2026-06-14T18:27:56.848Z
is_conflict: 0
latitude: 0.00000000
longitude: 0.00000000
altitude: 0.0000
author: 
source_url: 
is_todo: 0
todo_due: 0
todo_completed: 0
source: joplin-desktop
source_application: net.cozic.joplin-desktop
application_data: 
order: 1781287696480
user_created_time: 2026-06-12T18:08:16.480Z
user_updated_time: 2026-06-14T18:27:56.848Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 0
type_: 1