Gen 3 — Range-Aware Equity (State of the Art)

# Gen 3 — Range-Aware Equity

> **Current state, 2026-06-30.** Core postflop engine used by all production configs. Gen 4 wraps Gen 3 and feeds it better ranges and adaptive thresholds.

## Role in the Framework

Gen 3 is the postflop engine used by **all production configs**. Both `cash_nl.toml` (Gen 3 baseline) and `cash_nl_g4_live.toml` (Gen 4 production) use the Gen 3 postflop strategy — Gen 4 wraps Gen 3 and feeds it better ranges.

The `RolloutPostFlopGen3` struct is the most important file in the codebase for postflop decisions.

## Preflop Maneuver

**Identical to Gen 2** — uses the same shared components:

| Priority | Strategy | When |
|---|---|---|
| 1 | `g2_ke_hu_nash` | HU, 15-25 BB |
| 2 | `g1_short_stack_preflop` | ≤25 BB |
| 3 | `ng_preflop` | >25 BB |

## Postflop Maneuver

### Gen 3 Postflop: `g3_postflop_rollout`

The key innovation: **range-aware equity**. Instead of computing equity vs random opponent hands, Gen 3 predicts a range of likely opponent holdings and computes equity against that range.

**Decision flow:**
1. **Range prediction**: Use a static `PlayerModel` (26 probability fields: preflop_open_raise, flop_bet, flop_fold_to_bet, etc.) to predict the opponent's range width
2. **Range construction**: Build a weighted range of hands consistent with the opponent's actions so far this hand
3. **Equity computation**: Monte Carlo using only hands within the predicted range (not all 1326 hands)
4. **Decision**: Same threshold-based logic as Gen 2 (calls use `win_prob`, bets/raises use `hs`)

> **Note**: The `range_tightness_scale` threshold discount was **removed** (2026-06-29). It double-counted range narrowing — once in equity (narrower ranges → lower hs/win_prob), once in the threshold. Removing it fixed the 88-shove bug where thresholds went negative.

### The `Gen3Equity` Struct

```rust
pub struct Gen3Equity {
    hs: f64,           // Hand strength vs predicted range
    ppot: f64,         // Positive potential
    npot: f64,         // Negative potential
    nutpot: f64,       // Nut potential
    rpot: f64,         // Robustness
    win_prob: f64,     // EHS = hs*(1-npot) + (1-hs)*ppot
    range_width: f64,  // Effective range width (1.0 = full, <1.0 = narrowed)
}
```

### Pot Odds Gate

`should_raise()` includes:
1. **Large bet penalty**: facing bet >30% pot → threshold increases proportionally
2. **SPR penalty**: SPR >2 → threshold increases by up to 0.18
3. **Hard equity gate**: hs <0.60 + committing >100% pot → threshold ≥0.65

### Bet Sizing

Configurable bet fractions by street (current **live** values after post-discount sweep):
- **Flop**: `bet_flop_base = 0.15`
- **Turn**: `bet_turn_base = 0.40`
- **Value bets**: fraction based on hand strength above threshold
- **Protection bets**: smaller bets with vulnerable made hands
- **Bluffs**: `bluff_hs_max = 0.05` (only bluff with hs < 0.05)

## Configs

| Config | Chain | Notes |
|---|---|---|
| `cash_nl.toml` | ke_hu → short_stack → ng_preflop → g3_postflop_rollout | Gen 3 baseline (non-adaptive) |
| `cash_nl_normal.toml` | same chain, different thresholds | Tuned for normal VPIP tables |
| `cash_nl_loose.toml` | same chain, looser thresholds | Tuned for loose tables |
| `cash_nl_tight.toml` | same chain, tighter thresholds | Tuned for tight tables |

## Key Thresholds (current live values)

```
# Call decisions
call_base = 0.45
call_ppot_base = 0.25
call_w_draw = 0.75
call_w_pot_odds = 0.70

# Bet decisions (post-discount sweep)
bet_flop_base = 0.15
bet_turn_base = 0.40

# Raise decisions
raise_base = 0.80
raise_w_protection = 0.35
raise_w_draw = 0.50

# Bluff safety
bluff_hs_max = 0.05
semi_bluff_max_bet_ratio = 0.30

# Range construction (weight map)
narrow_center_offset = 0.2
strong_hands_fraction = 0.30
scurve_steepness_mult = 0.3
```

## Key Files

| File | Content |
|---|---|
| `holdem_bots/src/cash/rollout_postflop_gen3.rs` | Gen 3 strategy + Gen3Config + Gen3Equity |
| `holdem_bots/src/cash/thresholds.rs` | PostflopThresholds (80+ fields, Default, from_config_map) |
| `holdem_bots/src/common/player_model.rs` | Static PlayerModel (26 probability fields) |

## Open Issues

- **Static player model**: The `PlayerModel` uses fixed default frequencies — it doesn't adapt to specific opponents. **Addressed by Gen 4** (dynamic observer replaces static model).
- **Cold-start problem**: First decisions against unknown opponents use default ranges that may be inaccurate. **Addressed by Gen 4** (profile persistence + three-tier blend).
- **Single range**: Gen 3 predicts ONE range per opponent. Multi-way pots need separate ranges per active opponent. **Partially addressed by Gen 4** (per-opponent ranges in multiway).

## Where This Goes Next

Gen 4 wraps Gen 3 and replaces the static `PlayerModel` with a **dynamic observer** that tracks real opponent stats (VPIP, PFR, AF, bet frequencies) via EMA, then uses those stats to build more accurate ranges and adapt thresholds. Gen 5 will eventually replace the heuristic decision logic with a neural network.

id: 0f0e106e3da94b75a2223dd2b22aafca
parent_id: abc167de888d41bf9391c373e90dec8d
created_time: 2026-06-28T05:16:18.613Z
updated_time: 2026-06-30T05:31:08.948Z
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: 1782623778613
user_created_time: 2026-06-28T05:16:18.613Z
user_updated_time: 2026-06-30T05:31:08.948Z
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