docs(critical): preflop RvR degenerate Nash investigation#159
Open
amaster97 wants to merge 1 commit into
Open
Conversation
Root cause identified: `PreflopRvrState::initial` (`crates/cfr_core/src/preflop_rvr.rs:113-135`) IGNORES `config.initial_contributions` and unconditionally sets `contributions = [sb_blind, bb_blind]` from `config.small_blind`/`big_blind`. When the user's config passes `initial_contributions: [50, 100]` (the blinds), the downstream chip-flow math at every leaf (lines 489-492 and 580-583) double-subtracts the blinds, yielding `cs0 = cs1 = 0` and `pot_total = 0` at the root. Fold/equity payoffs collapse to ~0 BB so folding becomes dominant -> SB folds 100% J7o, BB folds 99.25% facing 3bb, KQs/98s defend 0%. The AA-vs-KK smoke test passed only because it uses the correct `initial_contributions: [0, 0]` (`crates/cfr_core/tests/preflop_rvr_smoke.rs:38`); the user's repro used `[50, 100]`. Two fixes proposed: hard-reject non-zero `initial_contributions` in `solve_hunl_preflop_rvr` (safest), or document the invariant. NO code fix auto-applied — user decides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
amaster97
added a commit
that referenced
this pull request
May 28, 2026
…lidation (#161) Audit of `cs = contributions - initial_contributions` bug pattern (PR #159 context) across all subgame solver entry points. Findings: - Bug scope: PARTIAL. Same `State::initial` pattern in TWO solvers (`preflop_rvr` AND `preflop`), but Python `HUNLConfig.__post_init__` blocks the trigger combination (`Preflop` + `initial_contributions != [0,0]`). Reachable only via Rust-direct callers bypassing Python. - Postflop solvers (`solve_hunl_postflop`, `solve_range_vs_range_*`, exploit BR-walk) are NOT affected — they use the correct postflop branch at `crates/cfr_core/src/hunl.rs:318`. - All shipped perf claims (#114, #139, #150, #157) STAND. Every bench either uses `initial_contributions=[0,0]` (degenerate-subtraction-safe) or the unaffected postflop solver. - Recommendation: fix `PreflopRvrState::initial` (preflop_rvr.rs:229-251) + `HUNLState::initial_preflop` (hunl.rs:382-414) to honor `config.initial_contributions`, and add a Rust-side `HUNLConfig::validate()` mirroring Python's `__post_init__`. No engine code touched — surface-only audit. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 28, 2026
amaster97
added a commit
that referenced
this pull request
May 28, 2026
#164) Empirical investigation of degenerate preflop strategy hypothesis. 100-iter (post PR #157 .so) results with initial_contributions:[50,100] config: - SB J7s root: 99.99% fold (target: 20-40% fold) - BB KdQd vs 3bb: 71.71% fold (target: <=20% fold) - BB 8s9s vs 3bb: 98.76% fold (target: ~40% fold) - Trajectory: 92.12% (10 iters) -> 99.99% (100 iters); strategy NARROWS to fold VERDICT: bug confirmed, NOT a convergence issue. Direction of travel is toward more fold not less, consistent with PR #159 RCA (leaf payoffs collapse to ~0 when initial_contributions matches the blinds, so folding becomes the no-loss dominant action). 10000-iter run was launched in parallel (still in flight at commit time at ~47 min wall, slower per-iter rate than 100-iter sample suggested); doc will be amended with full numbers when summary lands. Engine code NOT modified. Empirical measurement only. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
amaster97
added a commit
that referenced
this pull request
May 28, 2026
…ns (#67) (#165) Root cause documented in PR #159 and audited in PR #161: both `PreflopRvrState::initial` and `HUNLState::initial_preflop` unconditionally set `contributions=[SB+ante, BB+ante]` regardless of `config.initial_contributions`. Downstream leaves compute `cs = contributions - initial_contributions`, so a caller passing `initial_contributions=[SB,BB]` with `initial_pot=0` (intending to declare posted blinds) produced `cs=[0,0]` and `pot_total=0` at every leaf — collapsing the preflop Nash to fold-everywhere. Fix: - `State::initial` honors `initial_contributions`: `contribution[i] = max(blind_amount[i], initial_contributions[i])`. Engine-posts-blinds (`[0,0]+pot=0`) and caller-declared-blinds (`[SB+ante,BB+ante]+pot=SB+BB+2*ante`) configs both produce Nash-equivalent strategies (per-leaf payoffs differ only by a constant per-player shift). - Add Rust-side `HUNLConfig::validate()` mirroring `poker_solver/hunl.py:124-176` (defense-in-depth per `feedback_silent_skip_hazard` — PyO3 callers bypass Python's `__post_init__` guard). - Wire `validate()` into `solve_hunl_preflop_rvr` so malformed configs (e.g., `[50,100]+pot=0`) fail loudly instead of returning a degenerate fold-everywhere equilibrium. Tests: - `crates/cfr_core/tests/preflop_initial_contributions.rs` (11 Rust tests): AA-does-not-fold regression, identical-strategies diff-test between both well-formed configs, eight `HUNLConfig::validate()` accept/reject cases, and an entry-point rejection test for the bug-triggering config. - `tests/test_preflop_rvr_diff.py::test_engine_agnostic_to_blind_declaration`: Python-side parametrized variant of the diff-test (max_drift < 1e-4 between the two config shapes through the PyO3 boundary). Existing tests verified clean: - AA-vs-KK closed-form smoke (`preflop_rvr_smoke.rs`) still PASSes. - All 4 `test_preflop_rvr_diff.py` Python diff tests PASS. - `cargo test -p cfr_core --lib`: 75 unit tests PASS. - `cargo clippy --lib --tests`: clean. - `ruff check`: clean. Postflop branch (`hunl.rs:318`) unchanged — it was already correct. Leaf-payoff math unchanged — only `State::initial` and `validate()`. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
amaster97
added a commit
that referenced
this pull request
May 28, 2026
1000-iter rerun (10x original baseline, post-PR #157 .so): - Wall: 179.49 s (0.1795 s/iter) - SB J7s root: 100.00% fold - SB AA (AsAd) at root: [0.0, 0.0016, 0.3737, 0.4103, 0.1331, 0.0806, 0.0006] (100% non-fold, plays normally — confirms bug only affects fold-at-root leaves) - BB aggregate vs 3bb: 99.25% fold (1225 unblocked hands) - BB KdQd vs 3bb: 99.97% fold - BB 8s9s vs 3bb: 100.00% fold Trajectory: 10 iters (92% J7s fold) -> 100 iters (99.99%) -> 1000 iters (100%). Strategy monotonically narrows to fold, never broadens — opposite of what a convergence issue would show. 10000-iter run was launched but killed externally at ~50 min wall before writing its summary (suspected macOS memory pressure, peak RSS ~4 GB). Verdict does not depend on it: the trajectory at 10/100/1000 iters is unambiguous. VERDICT: BUG CONFIRMED, NOT a convergence issue. Bumping default iter count will NOT fix this. Leaf math must be fixed per PR #159. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Root cause identified for the degenerate Nash equilibrium in
_rust.solve_hunl_preflop_rvr(PR #122) where SB folds J7o 100% and BB folds 99.25% facing 3bb at 40 BB / 1000 iters.The bug:
PreflopRvrState::initial(crates/cfr_core/src/preflop_rvr.rs:113-135) IGNORESconfig.initial_contributionsand unconditionally setscontributions = [sb_blind, bb_blind]fromconfig.small_blind/big_blind. When the user's repro config passesinitial_contributions: [50, 100](matching the blinds), the downstream chip-flow math at every leaf (preflop_rvr.rs:489-492equity,:580-583fold) double-subtracts the blinds:Fold/equity payoffs collapse to ~0 BB at root, so folding becomes dominant. The AA-vs-KK smoke test passed only because it uses the correct
initial_contributions: [0, 0](preflop_rvr_smoke.rs:38).Suggested fix (NOT applied — user decides)
Hard-reject non-zero
initial_contributionsinsolve_hunl_preflop_rvr(lines 1050-1059 already validateinitial_hole_cards; add a parallel check). The full-tree preflop solver only has one valid root-state shape; rejecting other shapes prevents this class of silent miscalibration.Test plan
/tmp/test1_40bb_1000iter.pywithinitial_contributions: [0, 0]and verify SB opens J7o >50%, BB defends KQs ~100%aa_vs_kk_closed_formfor a J7o vs defending-range pairpreflop_rvr_smoke.rs) continues to pass under either fix🤖 Generated with Claude Code