docs: cs-bug scope audit (preflop-only vs broader) + perf claim re-validation#161
Merged
Conversation
…lidation 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>
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>
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
PreflopRvrState::initialhardcoding[SB,BB]while leaves docs = contributions - initial_contributions) extends to other solvers.HUNLState::initial_preflop(crates/cfr_core/src/hunl.rs:382-414) too, but PythonHUNLConfig.__post_init__(poker_solver/hunl.py:146-148) blocks the trigger combination. Postflop solvers are NOT affected (correct postflop branch athunl.rs:318).initial_contributions=[0,0](where the subtraction is degenerate-safe) or the unaffected postflop path.Test plan
crates/cfr_core/src/preflop_rvr.rs:229-251,crates/cfr_core/src/hunl.rs:318, 382-414,crates/cfr_core/benches/preflop_rvr_profile.rs:42-66, etc.)State::initialdispatch against its leaf-utility formulapoker_solver/hunl.py:146-148is the load-bearing safeguardDoc-only PR. Auto-merge OK.
🤖 Generated with Claude Code