Nash Solver Port — Analysis & SB/BB Mystery

# Nash Solver Port — Analysis & SB/BB Mystery

## Live Hand Review (2026-06-25 session)

### Quads hand (88) — cf06fbe8 — All decisions sound

Blinds $12.5k/$25k NL. Bolsa dealt [8d 8h] in CO.

1. **Preflop**: Raise to $137.5k (5.5BB) over 4 limpers from CO. 5 callers. Correct isolation raise.
2. **Flop [7s 2h 9s]**: Check behind in 6-way pot. 88 overpair is marginal multi-way. Defensible — a small c-bet for protection against flush draw would be marginally better.
3. **Turn [7s 2h 9s 8c]**: SB bets pot ($850k). Hero raises to $2.55M (3x). Top set (888), charges draws. Excellent.
4. **River [7s 2h 9s 8c 8s]**: AllIn $3.81M with quads (8888). Absolute nuts. Obviously correct.

**Opponent (Tronski) showed JsTc** — straight 7-8-9-T-J. **Quads beat a straight.** The opponent's claim that "he clearly had a straight" was irrelevant — a straight never beats quads. Tronski paid off $5.67M. Bot won $11.6M.

### Other notable hands
- **AA (c9a1baa)**: Raise → 4-bet → AllIn. Perfect, won $7.1M.
- **AQ (730d4c3)**: Top pair flop bet, two-pair turn AllIn. Split pot (both had AQ). Correct.
- **KJ (3f75a0c)**: Preflop *call* on BTN with KJ over 5 limpers is passive — should raise for value. River fold to pot-sized bet when 7-6-8-9 completes straight/flush — correct.

**No leaks found in the recent session.**

---

## Nash Solver Port — Complete

### Architecture (`holdem_bots/src/sng/nash_solver.rs`)

**Algorithm**: Iterative best-response dynamics (port of Java `SnGNashStrategy`):
1. Each iteration, for every player position, compute per-hand push equity vs fold equity
2. Set push boundary where E[push] crosses E[fold] (hands ordered strongest→weakest)
3. Same for call boundaries (E[call] vs E[fold] for each hand vs pusher's range)
4. Apply EMA smoothing: `new = best * lr + old * (1-lr)` where `lr = 1/(n + increase*ln(run+10))`
5. Converge when no rounded boundary changes

**Key components**:
- **169×169 HU equity table**: MC with 1,000 samples per matchup, parallelized via rayon. Computed once, cached in `OnceLock`. ~46 seconds first call.
- **Post-blind stack model**: Correctly converts pre-blind stacks to post-blind (deduct antes, SB, BB) before ICM calculation. Pot = sb + bb + antes*nplayers.
- **ICM integration**: Uses existing `crate::common::icm::calculate_icm_equity`.
- **Per-hand equity comparison**: Instead of maximizing overall range equity, directly compares E[push specific hand] vs E[fold] and finds the crossing point. Cleaner and more numerically stable.

**API**:
```rust
let mut solver = NashSolver::new(stacks, sb, bb, antes, payouts, config);
solver.solve();
solver.push_range_fraction(player);  // 0.0-1.0
solver.call_range_fraction(pusher, caller);
solver.hand_in_push_range(hi_rank, lo_rank, suited, player);
```

**Test results**: 9 tests, all pass.
- HU at 10BB: SB pushes >50% ✓
- 6-max at 10BB: LP pushes >5% ✓
- Short stack pushes wider than big stack ✓
- HU pushes wider than 6-max ✓
- AA always in range ✓

### Usage (planned)
- **Gen 1**: Quick static tables (SAGE, push/fold lookup). No solver.
- **Gen 2+**: Use Nash solver for SNG/MTT push/fold decisions. Solver runs at hand start (once per blind level change), results cached.
- Integration point: `nash_icm.rs` — replace static PUSH_RANGES/CALL_RANGES with solver-computed ranges.

---

## The SB/BB Mystery — Why the manual fix improved testbed performance

### Context
In the Java codebase, manually tweaking SB (and possibly BB) ranges in the Nash solver output improved testbed performance. Nash solver vs. Nash solver with SB/BB fix had a clear edge for the "fixed" version. This was never understood.

### Likely explanation

The iterative best-response solver computes ranges assuming all opponents play Nash equilibrium. But in the **testbed**, opponents are NOT playing perfect Nash — they play the bot's own strategy (which has imperfections) or heuristic opponents.

The SB/BB fix likely works because of **position asymmetry in the best-response dynamics**:

1. **SB acts first in the push/fold game.** When the solver iterates, it updates push ranges in order (player 0 first, then 1, etc.). The SB (second-to-last position) is updated LATE in each iteration. By the time SB's range is optimized, all other players' ranges have already been updated for this iteration. This creates a **sequential dependency bias** — later positions get slightly more "current" information about earlier positions' strategies.

2. **BB is last to act and has a special role** — they close the action. The BB's calling range is the most complex (faces pushes from all positions). Small errors in the BB's call range amplify because they affect ALL pushers' equities.

3. **ICM pressure is non-linear near the bubble.** The bubble factor (ratio of tournament equity lost to equity gained) is highest for medium stacks. SB and BB are often medium-stacked in SNG. A manual fix that tightens or loosens their ranges to account for real-world opponent deviations from Nash would improve performance against non-Nash opponents.

4. **The "fix" may have been compensating for a convergence artifact.** The iterative solver may not fully converge for SB/BB positions (they're updated last and with the most complex equity calculations). The manual fix could be pushing the ranges closer to true equilibrium than the solver achieves in limited iterations.

### Verification plan
- Run the Rust solver with increased iterations (1000+) and compare SB/BB ranges to the Java output with the manual fix
- Test both versions in the testbed (solver-only vs solver+fix)
- Check if the convergence criterion (no rounded boundary changes) is being met too early for SB/BB positions

---

## Next steps
1. **Wire solver into nash_icm.rs** — replace static tables with solver output at hand start
2. **Optimize equity table** — pre-compute at startup, or use faster evaluator (caching 7-card evaluations)
3. **Test in simulation** — A/B test solver-based SNG bot vs static-table SNG bot
4. **Investigate SB/BB convergence** — check if solver converges differently for SB/BB positions

id: 290e75ff16f6485ab1f6f827b99078fb
parent_id: 1246bbc3bb4948fc8329079b84b4ae3d
created_time: 2026-06-25T13:20:44.352Z
updated_time: 2026-06-25T13:20:44.352Z
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: 1782393644352
user_created_time: 2026-06-25T13:20:44.352Z
user_updated_time: 2026-06-25T13:20:44.352Z
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