Session Audit & Sweep Results — 2026-06-28

# Session Audit & Sweep Results — 2026-06-28

> Full-session work: live server audit, S3 fix (preflop looseness wiring),
> Gen 2/3/4 loose/tight configs, Gen 4 opponent model sweep, Gen 3 config
> comparison, Gen 5 architecture review.

---

## 1. Live Server Status

- **Server**: `live_server` on `127.0.0.1:8088` (rebuilt with S3 fix)
- **Default bot**: `cash_nl_g4_live` (Gen 4 adaptive)
- **Registered configs**: 8 total (g4_live, g4, g4_loose, g4_tight, normal, loose, tight, g2)
- **Profile persistence**: working — table + per-player profiles saved/loaded on restart
- **S3 fix active**: preflop looseness now wired into NG preflop thresholds

---

## 2. S3 Fix: Preflop Looseness Wired (CODE CHANGE)

**Issue**: `g4_adaptive_preflop` computed a VPIP-driven looseness factor but
delegated directly to NG preflop without applying it. The looseness functions
were `#[cfg(test)]` only.

**Fix applied**:
1. Added `looseness_mult: f64` field to `NgPreFlopConfig` (default 1.0, parsed from TOML)
2. NG preflop multiplies open-raise and limp thresholds by `looseness_mult` (clamped 0.5–2.0)
3. `AdaptivePreflopGen4::get_action` now computes the looseness factor from table VPIP and applies it via a per-decision config clone
4. Looseness mapping: VPIP 0.15→0.85, 0.30→1.0, 0.50→1.15, 0.70→1.30

**Files changed**:
- `holdem_bots/src/cash/ng_preflop.rs` — config field + threshold application + `config()` accessor
- `holdem_bots/src/gen4/adaptive_preflop.rs` — removed `#[cfg(test)]`, wired into `get_action`

**Tests**: all 10 existing tests pass (looseness monotonicity, clamping, cold-start)

---

## 3. Baseline Gen 4 Audit (10k hands, seed 42)

6-max (2 flock + 2 chump + 1 test bot), 50k MC samples:

| Player | Won | Profit/Hand | ROI% |
|--------|-----|-------------|------|
| G4Live | 1202 | **+144.65** | +72325% |
| FlockBot-1 | 820 | +100.61 | +50307% |
| ChumpBot-1 | 3141 | -200.99 | -100493% |

**Range prediction accuracy**: 87.9% hit rate (12892/14661 showdowns)
- Flop: 88.7% | Turn: 88.6% | River: 87.8%
- All opponents classified as Loose (>=0.45 VPIP) — no tight opponents in this field

---

## 4. Gen 4 Opponent Model Sweep (14 variants × 3 seeds × 20k hands)

**Methodology**: Fixed ALL decision thresholds at `cash_nl_g4_live.toml` values.
Swept ONLY the weight map + narrowing curve parameters to test opponent model
accuracy without changing when the bot calls/bets/raises.

### Results (profit/hand vs chump+flock field)

| Variant | Param Changed | Profit/Hand | Δ vs Baseline |
|---------|--------------|-------------|---------------|
| **offset030** | narrow_center_offset=0.3 | **152** | **+2** |
| frac035 | strong_hands_fraction=0.35 | 151 | +1 |
| scurve05 | scurve_steepness_mult=0.5 | 151 | +1 |
| combo_wide | frac=0.25, looseness=0.10, offset=0.3, str=1.2 | 151 | +1 |
| **baseline** | live config | **150** | — |
| frac025 | strong_hands_fraction=0.25 | 149 | -1 |
| frac020 | strong_hands_fraction=0.20 | 150 | 0 |
| loose010 | range_looseness=0.10 | 148 | -2 |
| loose015 | range_looseness=0.15 | 150 | 0 |
| offset010 | narrow_center_offset=0.1 | 148 | -2 |
| strength08 | narrow_strength_mult=0.8 | 149 | -1 |
| strength12 | narrow_strength_mult=1.2 | 149 | -1 |
| scurve02 | scurve_steepness_mult=0.2 | 150 | 0 |
| combo_tight | frac=0.20, looseness=0.02, offset=0.1, str=0.8 | 148 | -2 |

### Key Findings

1. **Weight map parameters have minimal impact (±2/hand)** when thresholds are
   fixed. The opponent model is already accurate at ~88%.

2. **Narrowing center offset is the strongest lever**: 0.3 (+2) > 0.2 (baseline)
   > 0.1 (-2). Wider offsets = less aggressive range narrowing = better.

3. **Strong_hands_fraction is nearly flat**: 0.20-0.35 all within ±1/hand.
   The adaptive observer's per-opponent VPIP-based adjustment already handles this.

4. **Range looseness above 0.10 hurts**: adding too many medium-tier hands to the
   weight map dilutes range accuracy (148-150 vs baseline 150).

5. **Narrowing strength doesn't matter** (confirms Gen 4 deep sweep): 0.8, 1.0,
   1.2 all give 149-150. The S-curve shape absorbs the difference.

**Conclusion**: The opponent model is well-calibrated. The real performance
lever is the **threshold scaling** (`range_tightness_scale`), not the weight map
shape. This validates the design philosophy: "make the opponent model accurate,
let thresholds stay fixed."

---

## 5. Gen 3/4 Config Comparison (6 configs × 2 fields × 3 seeds × 20k hands)

| Config | Chump Field | Flock Field | Sum |
|--------|------------|------------|-----|
| **cash_nl_g4_loose** | **150** | 0 | **150** |
| cash_nl_g4_live | 148 | 1 | 149 |
| cash_nl_g4_tight | 148 | 2 | 150 |
| cash_nl_loose (G3) | 86 | 4 | 90 |
| cash_nl_normal (G3) | 72 | 3 | 75 |
| cash_nl_tight (G3) | 69 | 3 | 72 |

### Key Findings

1. **Gen 4 dominates Gen 3 by ~70%** (148 vs 86 vs chumps). The adaptive
   observer + range narrowing + threshold scaling is the single biggest
   improvement in the framework.

2. **Loose/tight differentiation matters more for Gen 3** than Gen 4.
   Gen 3 loose (86) vs tight (69) = 17-point spread. Gen 4 loose (150) vs tight
   (148) = 2-point spread. The adaptive layer absorbs the table-type differences.

3. **`cash_nl_g4_loose`** is marginally best vs calling stations (+2/hand over live).
   Its starting thresholds favour thin value betting, which the observer further
   refines per-opponent.

4. **All configs struggle vs flock field** (0-4/hand). Flock_bot (tight-passive)
   is a near-break-even opponent. This is expected — beating tight opponents
   consistently is much harder than beating calling stations.

---

## 6. New Configs Created

| Config | Generation | Purpose |
|--------|-----------|---------|
| `cash_nl_gen2_loose.toml` | Gen 2 | Calling-station tables (value_bet_floor=0.55, wider calls) |
| `cash_nl_gen2_tight.toml` | Gen 2 | Skilled tables (value_bet_floor=0.70, tighter calls, more bluffs) |
| `cash_nl_tight.toml` | Gen 3 | Very tight tables (bet_flop=0.25, call_base=0.42, fold_freq=1.0) |
| `cash_nl_g4_loose.toml` | Gen 4 | Loose starting point (bet_flop=0.35, call_base=0.50, narrow_strength=1.2) |
| `cash_nl_g4_tight.toml` | Gen 4 | Tight starting point (bet_flop=0.25, call_base=0.42, bluff_hs=0.03) |

All registered in `configs/live_bots.toml` for A/B testing.

---

## 7. Gen 5 Architecture Review

### What's Well-Designed

1. **280-dim feature vector** — comprehensive (equity, board, betting, opponents, history)
2. **11-action discrete space** — good bet sizing granularity with proper legality masking
3. **Teacher-fallback pattern** — gracefully delegates to Gen 3/4 when no model loaded
4. **JSONL transition recorder** — thread-safe, for imitation learning data collection

### Architectural Concerns

| # | Issue | Severity | Fix |
|---|-------|----------|-----|
| G5-1 | **Opponent features not bridged** — `compute_features()` passes `&[]` for opponent_stats. The entire point of Gen 5 (using the opponent model) is disconnected. | High | Bridge Gen 4 `PlayerStats` → `OpponentFeatures` in `RlStrategy::compute_features()` |
| G5-2 | **No reward assignment** — `Transition` struct lacks a reward field. Self-play RL (PPO) is impossible without it. Only imitation learning works. | High | Add reward field + `on_hand_end` callback to assign outcomes |
| G5-3 | **`win_prob` missing from equity features** — features include hs/ppot/npot/nutpot/rpot but NOT `win_prob` (EHS), the most important call-decision metric. | Medium | Add win_prob as 6th equity feature (FEATURE_DIM bump to 281) |
| G5-4 | **No showdown hand recording** — can't train range-prediction NN (Phase 2) without labeled showdown data. | Medium | Record revealed hole cards at showdown |
| G5-5 | **`pot_after_bb` always 0.0** in action history features. | Low | Compute from wagered amounts |
| G5-6 | **Bet sizing too coarse** — only 5 bet fractions + 2 raise sizes. Limits nuanced sizing. | Low (Phase 2) | Consider hybrid continuous action (discrete + size parameter) |
| G5-7 | **MC equity bottleneck** — 50k samples per decision (~15ms) limits training throughput. | Low | Cache per (hole, board, range_hash) or use faster estimator |

### Overall Assessment

The architecture is **sound and pragmatic**. The MLP actor-critic with teacher
fallback is the right starting point. The two-phase approach (imitation →
self-play) is standard and well-justified. The recommendation to train in
Python/PyTorch while generating data in Rust is correct — candle for inference only.

**Immediate priorities for Gen 5 activation:**
1. Fix G5-1 (bridge opponent features) — trivial, ~30 min code change
2. Fix G5-2 (reward assignment) — needed for any RL training
3. Fix G5-3 (win_prob feature) — needed for call-decision quality
4. Wire Gen5 recorder into live server alongside Gen 4 decisions

---

## 8. Summary of Open Issues Status

| ID | Issue | Status |
|----|-------|--------|
| **S1** | Passive postflop play | Partially addressed — `bet_turn_base=0.58`, `strong_hands_fraction=0.30`. Further improvement needs Gen 5 (learned betting frequencies). |
| **S2** | Cold start waste | **Resolved** — `update_table_info()` loads table profile from disk; `on_hand_start()` loads player profiles. Verified in code. |
| **S3** | Preflop looseness not wired | **Fixed** — looseness factor now applied to NG preflop open-raise + limp thresholds. |
| **S4** | Range prediction 13% miss | Open — 87.9% hit rate. Weight map sweep shows minimal improvement possible via parameter tuning. Needs Gen 5 range-prediction NN. |
| **U5** | clickWhenReady hand ID validation | Open (userscript side) |
| **D1** | Missing ante events | Open (userscript side) |

---

## 9. Recommendations for Next Session

1. **Deploy `cash_nl_g4_loose` as the default** for Torn (calling-station field).
   Switch to `cash_nl_g4_tight` if the table VPIP drops below 0.25.

2. **Activate Gen 5 data collection** — fix G5-1/G5-2/G5-3, wire recorder into
   live server, start collecting transitions for imitation learning.

3. **Sweep EMA half-life** — current `ema_half_life=100` may be too slow for
   live adaptation. Try 50 and 75 to see if faster opponent adaptation helps.

4. **Focus Gen 4 improvement on threshold scaling**, not weight map shape.
   The sweep proves the model is accurate; the bottleneck is how thresholds
   compensate for narrowed ranges.


id: 4d53750895be4a30af66ec1c2aea161e
parent_id: e13f1845de9b4b6392ad866354fbd562
created_time: 2026-06-28T07:46:23.064Z
updated_time: 2026-06-30T05:34:40.132Z
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: 1782632783064
user_created_time: 2026-06-28T07:46:23.064Z
user_updated_time: 2026-06-28T07:46:23.064Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 1782797680132
type_: 1