Gen 4 — Adaptive Table & Player Models (Implementation Plan)

# Gen 4 — Adaptive Table & Player Models

## Goal

Replace static config profiles (cash_nl, cash_nl_normal, cash_nl_loose) with a single adaptive strategy that learns opponent behavior from observed play. Profiles persist across sessions as JSON files.

## Architecture

```
Event Stream → Observation Engine → TableProfile + PlayerProfile(s)
→ Range Inference (VPIP/AF → per-opponent HandRange)
→ Config Interpolation (table stats → floating Gen3Config params)
→ Gen3 Equity (per-opponent MC) + Decision (floating thresholds)
→ Prediction Logging at showdown
```

## Data Model

### PlayerProfile (per opponent, keyed by name + game context)

Stats tracked via EMA (exponential moving average) with sample_count:
- **VPIP** — voluntarily put money in pot preflop
- **PFR** — preflop raise frequency  
- **AF** — postflop aggression factor (bets+raises)/calls
- **Fold-to-Bet** — fold frequency facing postflop bets
- **Fold-to-Cbet** — fold facing continuation bets
- **Cbet freq** — bet flop after raising preflop
- **3-bet frequency**
- **WTSD** — went to showdown
- Showdown hand histogram (for range validation)

### TableProfile (aggregate across all players)

Same stats pooled across all observed players. Converges faster (~20-30 hands) because data is shared.

### Storage Layout

```
profiles/
  cash_nl_500_1000/
    table_torn-holdem.json
    player_Bolsa.json
    player_xxShadowsFallxx.json
  sng_9max/
    table_table1.json
    player_Alice.json
```

Keyed by {format, stakes, player_name} — same player at different stakes gets separate profiles.

### EMA Half-Life
- Live play: 100 hands (players adjust over time)
- Simulation: 30 hands (faster convergence for testing)

## Three-Tier Fallback

```
hands < 5     → Default (Gen3Config)
5 ≤ h < 20    → Table model (pooled aggregate)
20 ≤ h < 50   → Blend player × table (linear weight)
hands ≥ 50    → Player model dominates
```

## Range Inference: VPIP → HandRange

```
VPIP 15% (rock):     frac=0.20, looseness=0.02, floor=0.0
VPIP 30% (standard): frac=0.35, looseness=0.10, floor=0.02
VPIP 45% (loose):    frac=0.45, looseness=0.25, floor=0.05
VPIP 70% (maniac):   frac=0.65, looseness=0.40, floor=0.10
```

AF adjusts draw weight: passive (0.3) → 0.5, normal (1.5) → 1.0, aggressive (3.0) → 1.5

## Config Interpolation: Floating Parameters

**Float with table stats:**
| Parameter | Driver | Tight→Loose |
|---|---|---|
| strong_hands_fraction | VPIP | 0.25→0.50 |
| range_looseness | VPIP | 0.02→0.35 |
| fold_freq_multiplier | F2B | 0.25→1.0 |
| bluff_factor | F2B | 3.0→1.5 |
| call_base | AF | 0.50→0.38 |
| bet_value_fraction | VPIP | 0.60→0.75 |

**Stay static (universal poker math):**
- bet_flop_base, bet_turn_base, raise_w_draw, call_w_draw, raise_w_protection, call_w_pot_odds

## Per-Opponent Range Computation

Each opponent gets their own HandRange based on their effective VPIP/AF.
- **Heads-up**: exhaustive enumeration with opponent's range (unchanged)
- **Multi-way**: MC with per-opponent ranges (importance sampling — sample uniformly, weight by individual range membership)
- Fast path: if all ranges identical → existing shared MC

## Preflop Adjustments

Position-based thresholds in ng_preflop float with table VPIP:
- Tight table → steal wider, call tighter
- Loose table → steal narrower, call looser for implied odds

Multiplier = `(table_vpip / 0.30).clamp(0.7, 1.4)` applied to opening/stealing ranges.

## Prediction Measurement & Logging

At decision time: log predicted ranges, equity, config source (default/table/player blend).
At showdown: validate — was shown hand in predicted range? Track running accuracy.
Metrics: in_range_rate, mean_percentile_error, bias (too tight vs too loose).

## Implementation Steps (11 steps)

1. **Profile data structures** — EmaStat, PlayerStats, PlayerProfile, TableProfile, range inference, config interpolation
2. **JSON persistence** — serialize/deserialize, path construction, graceful fallback
3. **Observation engine** — consumes game events, updates EMA stats (VPIP/PFR/AF/F2B)
4. **Per-opponent range computation** — extend MC to accept individual ranges
5. **Adaptive strategy wrapper** — composes observer + Gen3 with floating config
6. **Gen3 refactor** — accept external equity + config (separate computation from decision)
7. **Preflop adjustments** — VPIP multiplier on ng_preflop thresholds
8. **Prediction logging** — decision logs + showdown validation + accuracy tracker
9. **Testing harness** — mixed bot tables (chump/flock/gen2/gen3), A/B adaptive vs static
10. **Live server integration** — observer per table, feed events, save/load profiles
11. **Registration & config** — g4_adaptive strategy type, adaptive_mode toggle

## Testing

- **Unit**: EMA convergence, range interpolation, blending, JSON round-trip
- **Integration**: event sequence → profile → range prediction → decision
- **Simulation**: 4 table compositions × 10k hands × 20 seeds, A/B adaptive vs static
- **Key validation**: adaptive hero correctly exploits chump_bot (value bet, no bluff) and flock_bot (bluff more, call lighter)

## Migration

Gen 4 runs alongside Gen 3. `adaptive_mode = false` (default) = Gen 3. Once validated, flip to true.

Full plan with code sketches: `.kilo/plans/gen4-adaptive-models.md`

id: 88c7a2bd92a64a459996ffd2ae01ee04
parent_id: abc167de888d41bf9391c373e90dec8d
created_time: 2026-06-25T08:31:37.434Z
updated_time: 2026-06-30T05:34:40.039Z
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: 1782376297434
user_created_time: 2026-06-25T08:31:37.434Z
user_updated_time: 2026-06-25T08:31:37.434Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 1782797680039
type_: 1