Gen 3 Baseline — Implementation Plan

# Gen 3 Baseline — Port NGPostFlopStrategyV3 to Rust

## Goal
Port the Java Gen 3 postflop strategy (`NGPostFlopStrategyV3`) to Rust as a new
strategy `g3_postflop_rollout`. The key Gen 3 innovation: equity is computed
against **predicted opponent card holdings (weighted maps)** instead of uniform
random hands. Additionally, integrate rpot/nutpot considerations from the
existing Rust nut-aware infrastructure.

## Source Files (Java)
- `sng/supersonic/development/NGPostFlopStrategyV3.java` — 1358 lines (main strategy)
- `sng/supersonic/development/NGShortHandedPostFlopStrategyV3.java` — 53 lines (override)
- `common/supersonic/RangePredictor.java` — 477 lines (range tracking)
- `common/supersonic/HandRange.java` — 290 lines (weighted range)
- `common/supersonic/PlayerModelV3.java` — 966 lines (opponent statistics)

## Target Files (Rust) — 7 new/modified files

### Phase 1: Weighted Range Infrastructure
**NEW: `holdem_bots/src/common/hand_range.rs`**

Port `HandRange` from Java. A weighted map of all possible 2-card holdings.

```rust
pub struct HandRange {
    weights: [[f64; 52]; 52],  // weights[card_a][card_b] = weight
}
```

Methods to implement:
- `new()` — all zeros
- `init_all(weight)` — set all pairs to `weight`
- `clear_card(card)` — zero out all pairs containing `card`
- `init_strong_hands(board_cards, hole, top_fraction)` — top X% by hs on board
- `init_strong_draws(board_cards, hole)` — flush draws + OESDs + two overcards
- `init_top_preflop_hands(remaining, fraction)` — top X% by preflop ranking
- `unset_low_ranking_postflop_hands(board, remaining, top_fraction)` — filter weak
- `set_pocket_pairs(remaining, fraction)`
- `set_suited_aces(remaining, fraction)`
- `set_suited_connectors(remaining, fraction)`
- `get_weight(c1, c2) -> f64`
- `set_weight(c1, c2, w)`
- `sum() -> f64`
- `is_empty() -> bool`

Dependencies:
- Needs `HandPotential::compute_hs_for_cards()` to evaluate hs for each pair (for init_strong_hands)
- Needs hand classification helpers (is_flush_draw, is_oesd, is_two_overcards) for init_strong_draws

### Phase 2: Weighted Equity Computation
**MODIFY: `holdem_bots/src/common/hand_potential.rs`**

Add weighted computation methods:

```rust
impl HandPotential {
    /// Compute hs/ppot/npot/nutpot/rpot weighted by an opponent HandRange.
    pub fn compute_vs_range(
        &self, hole: &[Card; 2], board_cards: &[Card], range: &HandRange,
    ) -> PotentialResult { ... }

    /// Compute hs for a specific pair of cards (used by HandRange::init_strong_hands).
    pub fn hs_for_cards(
        &self, c1: Card, c2: Card, board_cards: &[Card],
    ) -> f64 { ... }
}
```

The `compute_vs_range` method is structurally identical to the existing
`compute_flop`/`compute_turn`, but each opponent pair (i,j) is weighted by
`range.get_weight(remaining[i], remaining[j])` instead of 1.0. This affects:
- The HP[][] transition matrix (weighted counts)
- The nutpot/rpot accumulators (weighted completion tracking)
- The hs computation (weighted ahead/tied/behind counts)

Performance: ~3× the Gen 2 cost. With incremental trie + rayon, ~15-20ms/decision.

### Phase 3: Default Player Model
**NEW: `holdem_bots/src/common/player_model.rs`**

Port `PlayerModelV3` default reference values and methods.

```rust
pub struct PlayerModel {
    // Preflop stats
    preflop_open_raise: f64,      // 0.104
    preflop_raise_limpers: f64,   // 0.095
    preflop_3bet: f64,            // 0.045
    preflop_4bet: f64,            // 0.053
    // Flop stats (8 fields)
    flop_conti_bet: f64,          // 0.729
    flop_bet: f64,                // 0.305
    flop_raise_cbet: f64,         // 0.162
    flop_raise: f64,              // 0.097
    flop_3bet: f64,               // 0.081
    flop_4bet: f64,               // 0.294
    flop_fold_to_conti_bet: f64,  // 0.654
    flop_fold_to_bet: f64,        // 0.756
    // Turn stats (7 fields)
    // River stats (7 fields)
}
```

Methods:
- `default_reference() -> Self` — early-stage defaults
- `default_short() -> Self` — short-handed stage defaults
- `default_hu() -> Self` — HU stage defaults
- `model_modifier(ctx, aggressor_seat) -> f64` — 0.75–1.33, sqrt-clamped
- `all_fold_to_conti_bet(ctx) -> f64` — compound fold probability
- `all_fold_to_bet(ctx) -> f64` — compound fold probability
- `someone_bets(ctx) -> f64` — probability of facing a bet

All use `ctx` for player/position info. Initial baseline uses default reference
values (no per-opponent tracking — that's Gen 4 territory).

### Phase 4: Gen 3 Postflop Strategy
**NEW: `holdem_bots/src/cash/rollout_postflop_gen3.rs`**

Port `NGPostFlopStrategyV3` decision logic (~800 lines).

```rust
pub struct RolloutPostFlopGen3 {
    base: RolloutPostFlopBase,
    model: PlayerModel,
    config: Gen3Config,
}

pub struct Gen3PotentialResult {
    hs: f64,                    // Raw hand strength (uniform)
    ppot_vs_ranges: f64,        // ppot vs strong hands (top 20%)
    npot_vs_ranges: f64,        // npot vs strong draws
    nutpot_vs_ranges: f64,      // nutpot vs strong hands (RUST ENHANCEMENT)
    rpot_vs_ranges: f64,        // rpot vs strong hands (RUST ENHANCEMENT)
    win_prob_vs_ranges: f64,    // EHS with range-aware ppot/npot
    win_prob_nut_aware: f64,    // Nut-aware EHS (RUST ENHANCEMENT)
}
```

Decision flow (matching Java NGPostFlopStrategyV3):

```
get_action(ctx):
  equity = compute_gen3_equity(ctx)
  strength = max(hs, win_prob_vs_ranges)
  if first_in (to_call == 0 && num_raises == 0):
    get_first_in_action(ctx, equity)
  else:
    get_raised_pot_action(ctx, equity)

get_first_in_action():
  1. Check-raise trap?
     strength^cr_adjustment > cr_threshold && random < cr_chance
  2. Conti-bet?
     i_am_preflop_aggressor && fold_prob > threshold && ppot < cap
  3. Semi-bluff?
     hs + ppot conditions + fold probability
  4. Luring bet?
     monster hand (hs ~0.98), small bet to induce
  5. Bluff bet?
     strength < bluff_threshold && fold_prob > 0.4
  6. Regular bet?
     strength^bet_adjustment > bet_threshold
  7. Check

get_raised_pot_action():
  1. Conti-bet defense?
     facing cbet from preflop aggressor, flop, heads-up
  2. Semi-bluff raise?
     same conditions as semi-bluff but as raise
  3. Rope-a-dope?
     monster, call to trap (strength^adj > threshold, random < chance)
  4. Raise?
     strength^raise_adj > raise_threshold
     || ppot > raise_ppot_threshold && num_active <= 3
     || raise_bluff_profitable()
  5. Call?
     strength^call_adj > call_threshold
     || ppot > call_ppot_threshold
     || pot_odds_good_enough
     || committed (push_when_calling)
  6. Fold
```

Threshold functions (all dynamic, using effective_m, board texture, player model):
- `determine_bet_threshold()` — Java L700-745
- `determine_raise_threshold()` — Java L768-805
- `determine_call_threshold()` — Java L848-882
- `determine_check_raise_threshold()` — Java L896-915
- `determine_check_raise_chance()` — Java L917-939
- `determine_rope_a_dope_threshold()` — Java L959-968
- `determine_rope_a_dope_chance()` — Java L970-979
- `determine_bluff_bet_threshold()` — Java L662-698

Bluff/deception tactics:
- `conti_bet()` + `get_conti_bet_action()` — Java L462-482
- `conti_bet_defense()` + `get_conti_bet_defense_action()` — Java L487-515
- `semi_bluff()` + `get_semi_bluff_action()` — Java L520-573
- `luring_bet()` + `get_luring_bet_action()` — Java L619-632
- `back_alley_mug()` + `get_back_alley_mug_action()` — Java L578-614
- `post_oak_bluff()` — Java L637-641
- `raise_bluff_profitable()` — Java L359-385

Utility methods:
- `scare_card_hit()` — Java L328-357 (compares strong hand ranges)
- `is_dangerous_board()` — npotVsRanges > threshold
- `amount_modification()` — Java L391-424 (round raises, handle near-allin)
- `push_when_calling()` — Java L1035-1046
- `pot_odds_good_enough_for_call()` — Java L832-846

NUT-AWARE ENHANCEMENTS (Rust-specific, per user request):
- Draw quality: `nutpot_vs_ranges / (nutpot_vs_ranges + rpot_vs_ranges + epsilon)`
- Adjusted ppot: `ppot_vs_ranges * (base_weight + quality_weight * draw_quality)`
- Nut-aware win_prob: `win_prob + (1-win_prob)*nutpot*0.5 - win_prob*rpot*0.5`
- Raise safety gate: block non-nut draws when npot > ceiling (from V3 F7)
- Use `max(hs, win_prob_nut_aware)` as the primary strength metric

### Phase 5: Config & Registration
**MODIFY: `holdem_bots/src/cash/mod.rs`** — add `rollout_postflop_gen3` module + re-export
**MODIFY: `holdem_bots/src/common/mod.rs`** — add `hand_range` + `player_model` modules
**MODIFY: `holdem_bots/src/assembly/registrations.rs`** — register `g3_postflop_rollout`
**NEW: `configs/bots/cash_gen3.toml`** — Gen 3 cash bot config

Gen3Config parameters (extracted from Java hardcoded values, sweepable):
```rust
pub struct Gen3Config {
    // Range construction
    pub strong_hands_fraction: f64,     // 0.20
    // Bluff parameters
    pub bluff_factor: f64,              // 2.0
    pub turn_conti_factor: f64,         // 1.0
    // Base thresholds (before effective_m / model adjustment)
    pub bet_flop_base: f64,             // 0.6
    pub bet_turn_base: f64,             // 0.83
    pub bet_river_base: f64,            // 0.9
    pub raise_base: f64,                // 0.93
    pub call_base: f64,                 // 0.75
    pub check_raise_base: f64,          // 0.95
    pub rope_a_dope_base: f64,          // 0.96
    // PPot thresholds
    pub raise_ppot_base: f64,           // 0.42
    pub call_ppot_base: f64,            // 0.38
    pub call_ppot_floor: f64,           // 0.20
    // Conti-bet parameters
    pub conti_bet_fold_threshold: f64,  // 0.315
    pub conti_bet_ppot_cap: f64,        // 0.35
    // Model modifier bounds
    pub model_modifier_min: f64,        // 0.75
    pub model_modifier_max: f64,        // 1.33
    // Nut-aware weights (Rust enhancement)
    pub nut_bonus_weight: f64,          // 0.5
    pub rpot_discount_weight: f64,      // 0.5
    pub draw_quality_epsilon: f64,      // 0.001
    pub npot_raise_ceiling: f64,        // 0.25
    // ... more as identified during implementation
}
```

### Phase 6: Verify
1. `cargo build --release`
2. Unit tests for HandRange, weighted equity, player model
3. Run: `./target/release/holdem_bots --config configs/bots/cash_gen3.toml --hands 10000`
4. Compare Gen 3 vs Gen 2 V3 win rates
5. Register in GUI for parameter sweeps

## Implementation Order
1. Phase 1: hand_range.rs (can test independently)
2. Phase 2: hand_potential.rs weighted methods (test vs known values)
3. Phase 3: player_model.rs (test default values)
4. Phase 4: rollout_postflop_gen3.rs (the big one)
5. Phase 5: registration + config
6. Phase 6: verify

## Estimated Effort
- Phase 1: ~300 lines, 2-3 hours
- Phase 2: ~200 lines, 2-3 hours (performance-sensitive)
- Phase 3: ~250 lines, 1-2 hours
- Phase 4: ~800 lines, 4-6 hours
- Phase 5: ~100 lines, 1 hour
- Phase 6: testing, 1-2 hours
- Total: ~1650 lines, ~12-17 hours

## Notes
- Keep Gen 2 V3 as-is — Gen 3 is additive
- Initial baseline uses default reference player model (no tracking)
- Nut-aware enhancements are Rust-specific improvements over the Java original
- All Java hardcoded constants extracted to Gen3Config for sweeping
- After baseline is working, apply structural improvements one-by-one

id: e7960a975c4f4d45b9472754cab5112c
parent_id: abc167de888d41bf9391c373e90dec8d
created_time: 2026-06-12T12:32:49.767Z
updated_time: 2026-06-22T15:54:32.732Z
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: 1781267569767
user_created_time: 2026-06-12T12:32:49.767Z
user_updated_time: 2026-06-12T12:32:49.767Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 1782143672732
type_: 1