id: d99ee4bf07c04cc1a0a18e8c132eb362
parent_id: 
item_type: 1
item_id: 29259ee3718a4bada4de157ef58cec3d
item_updated_time: 1781461676848
title_diff: "[{\"diffs\":[[1,\"Monte Carlo Multi-Way Equity Engine — Implementation Summary\"]],\"start1\":0,\"start2\":0,\"length1\":0,\"length2\":60}]"
body_diff: "[{\"diffs\":[[1,\"# Monte Carlo Multi-Way Equity Engine\\\n\\\n## Implementation\\\n\\\n### Hybrid approach\\\n- **Heads-up (1 opponent):** Exhaustive enumeration via `compute_vs_range` (exact, trie-optimized)\\\n- **Multi-way (2+ opponents):** Monte Carlo via `compute_vs_range_multiway_shared` (true multi-way equity)\\\n\\\n### Key methods\\\n\\\n#### `HandPotential::compute_vs_range_multiway_shared`\\\nProduction path — all opponents share a single range. Avoids cloning the 2704-entry HandRange once per opponent.\\\n\\\n```rust\\\npub fn compute_vs_range_multiway_shared(\\\n    &self,\\\n    hole: &[Card; 2],\\\n    board_cards: &[Card],\\\n    range: &HandRange,\\\n    num_opponents: usize,\\\n    num_samples: usize,\\\n    seed: u64,\\\n) -> PotentialResult\\\n```\\\n\\\n#### `HandPotential::compute_vs_range_multiway`\\\nOriginal per-opponent-range version. Still used by tests. Both delegate to shared `run_multiway_mc` core via `prepare_mc_deck` helper.\\\n\\\n#### `HandPotential::run_multiway_mc` (private core)\\\nShared MC loop used by both public methods. Takes pre-built sparse pair references.\\\n\\\n### MC Sampling Algorithm (optimized in round 2)\\\n\\\nFor each sample:\\\n1. Reset `present[]` mask — O(n) boolean array (replaces old O(n) card copy + swap_remove)\\\n2. Deal N opponent hands via `sample_sparse` — iterates precomputed sparse pair list O(k) instead of O(n²)\\\n3. Each opponent: filter by `present[i] && present[j]`, sample proportional to weight\\\n4. Mark dealt cards as `present[i] = false`\\\n5. Accumulate importance weight = product of normalization constants\\\n6. Rebuild `available` from present cards, shuffle, deal completion cards\\\n7. Evaluate hero vs **strongest opponent** (true multi-way equity)\\\n8. Classify HP transition (before → after) against strongest opp\\\n9. Accumulate weighted HP matrix\\\n\\\n### Precomputation (once per equity call, shared across all MC samples)\\\n- `nonzero_pairs_by_cards(cards)` → `Vec<(usize, usize, f64)>` — sparse list of in-range card-index pairs\\\n- For shared range: computed once, references repeated N times\\\n\\\n### Performance\\\n| Scenario | Evaluations | Time |\\\n|----------|------------|------|\\\n| 2 opp, M=20K | ~200K | ~20ms |\\\n| 5 opp, M=20K | ~300K | ~30ms |\\\n| 9 opp, M=20K | ~380K | ~38ms |\\\n| 9 opp, M=100K | ~1.9M | ~190ms |\\\n\\\nMC is faster than exhaustive even for HU (0.3ms vs 5ms), but we keep exhaustive for HU because it's exact.\\\n\\\n### Why not individual rollouts?\\\nProduct formula (hs₁ × hs₂ × ... × hsₙ) underestimates by 15-25pp due to positive correlation:\\\n- Top pair (hs=0.7 each): product = 0.34³ = 3.9% vs true MC ~28% for 9 opponents\\\n- ppot/npot don't extend to multi-way at all\\\n\\\n### Round 2 Optimizations\\\n\\\n| Change | Old | New | Benefit |\\\n|--------|-----|-----|---------|\\\n| MC sampling | `sample_weighted` rescans O(n²) per sample | `sample_sparse` over precomputed O(k) list | ~10-50× faster per sample |\\\n| Card removal | `swap_remove` on Vec (O(n) + index juggling) | `present[]` boolean mask (O(1) mark) | Simpler, no index bugs |\\\n| 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 |\\\n| OESD detection | Heap `Vec<u8>` allocation per pair | Fixed `[u8; 6]` stack array | Zero allocation |\\\n| Fallback result | Duplicated struct literals (3 sites) | `PotentialResult::fallback()` | DRY |\\\n| Board completion | Triplicated match block | `board_completion_count` const fn | DRY |\\\n| Guard+setup | Duplicated in both methods | `prepare_mc_deck` shared helper | DRY, prevents drift |\\\n| Union | Separate 52×52 loop | Delegates to `weighted_union(other, 1.0)` | DRY |\\\n\\\n### Test coverage\\\n- 24 MC tests in hand_potential.rs (matches exhaustive, multi-way < HU, river zero potential, empty ranges, deterministic, 9 opponents, conditional sampling)\\\n- 13 strategy tests in rollout_postflop_gen3.rs\\\n- Total: 340 tests pass, clean build, 0 warnings\\\n\\\n### Simulation results (10K hands, 6-max, seed 42)\\\n| Bot | Avg BB/Hand |\\\n|-----|-------------|\\\n| Gen3-2 | +3.31 |\\\n| Gen2-2 | +2.06 |\\\n| Gen3-1 | +1.75 |\\\n| Gen2-1 | -0.02 |\\\n| Gen1-2 | -2.66 |\\\n| Gen1-1 | -4.44 |\\\n\\\nGen3 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.\\\n\\\n## Next Steps\\\n1. **Round 2 sweep analysis** — 16 params × 3 values × 4 seeds = 192 runs (running)\\\n2. **RangePredictor** — per-opponent range tracking from observed actions\\\n3. **Gen 2 MC migration** — Gen 2 could use MC for multi-way (10-50× speedup)\\\n4. **Port missing tactics** — backAlleyMug, luringBet, scareCardHit\\\n5. **Board texture awareness** — connected/monotone/paired detection\"]],\"start1\":0,\"start2\":0,\"length1\":0,\"length2\":4557}]"
metadata_diff: {"new":{"id":"29259ee3718a4bada4de157ef58cec3d","parent_id":"1246bbc3bb4948fc8329079b84b4ae3d","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,"markup_language":1,"is_shared":0,"share_id":"","conflict_original_id":"","master_key_id":"","user_data":"","deleted_time":0},"deleted":[]}
encryption_cipher_text: 
encryption_applied: 0
updated_time: 2026-06-14T18:36:51.088Z
created_time: 2026-06-14T18:36:51.088Z
type_: 13