fix(engine): preflop State::initial honors config.initial_contributions (#67)#165
Merged
Merged
Conversation
…ns (#67) 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 was referenced May 28, 2026
amaster97
added a commit
that referenced
this pull request
May 28, 2026
…y check) (#168) Empirical comparison of `_rust.solve_hunl_preflop_rvr` output at 100 BB HU against a published GTO chart provided by the user, on top of the post-PR-#165 cs-bug fix. 10000-iter solve, ~5.8 min wall on M-series single-thread. Verdict: PARTIAL. Premium-value spots match (AA-JJ, AK*, A5s 4-bet jam at SB vs 3-bet -- 100% match on the high-EV-signal regions). Disagreements concentrate in (a) SB-limp action our engine permits but the published chart omits, (b) 3-bet sizing mismatch (closest engine output 8.5bb vs chart's 10bb under the task-spec multipliers), (c) Nash-multiplicity bluff cells (A5s/A2s flat-call vs chart's 3-bet bluff). Per-cell qualitative match: 24/37 = 64.9%; aggregate-frequency abs_diff > 5pp on 5/8 rows; median per-cell L1 = 0.000 (PASS); max L1 = 2.000 (FAIL, driven by limp/no-limp labeling artifact, not solver bug). Engine code untouched -- empirical measurement only. 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
Completes the user-requested J7o (and 27o, Test 4) 40 BB walkthrough first asked for prior to the cs-bug fix (PR #165) and True Path B engine (PR #171). All four tests use the 169-class True Path B fast engine (`_rust.solve_hunl_preflop_rvr_class169`) at 10000 DCFR iterations. Tests: - T1 baseline: SB opens J7o, BB calls, equity walk preflop->flop->turn->river - T2 3-bet/4-bet: SB J7o vs BB 3-bet vs SB 4-bet vs BB call line - T3 postflop raise: J7o equity vs BB's likely raise range on A89dd (engine is preflop-only; postflop equity from MC enumeration) - T4 off-distribution: 27o + 5x open, engine crash-check + defend dist Findings: J7o opens majority at 40 BB (matches published chart); SB prefers 2x over 3x open (GTO-consistent at 40 BB stack depth); 72o folds 100%; AA opens 100%; engine doesn't crash on off-tree play. Total wall: ~52s (29s solve + 23s postflop equity MC). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
amaster97
added a commit
that referenced
this pull request
May 28, 2026
Preemptive draft for v1.9.0 — ready for ship-trigger after audit clears. Covers the Premium-A blueprint shipping milestone: - 27 precomputed preflop blueprint shards (PR #171 + asset commit 1783bef): 169-class engine 178x/406x/448x speedup; ~21 MB bundle shipped under assets/blueprints/. - Blueprint loader API (PR #174), stack-depth interpolation (PR #173), postflop subgame wiring (PR #177), top-level SolverRouter (PR #181), UI integration (PR #178). - Vector-form BR walk (PR #170): W2.3 strict-PASS, persona table 17/0/0/0 (recorded in PR #184). - CS-bug fix (PR #165): preflop State::initial honors config.initial_contributions. - B10 per-combo frequency train (PRs #149/#154/#158/#160): W2.2 Sarah Range.diff -> PASS. - DCFR optimization ledger (docs/rust_optimization_ledger.md): empirical speedups for PRs #114/#139/#150/#157/#162/#170/#171. Known limitations disclosed honestly: - Flop live-solve OOMs in v1.9.0; flop = blueprint-only. v1.10 task #70 addresses (docs/v1_10_postflop_optimization_plan.md). - top_k_per_side is a speed knob, not a correctness flag. - "Realtime postflop" claim scoped precisely: turn + river only. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 28, 2026
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 documented in PR #159 and audited in PR #161. Both broken
State::initialmethods unconditionally hardcodedcontributions=[SB+ante, BB+ante]regardless ofconfig.initial_contributions; downstream leaves computecs = contributions - initial_contributions, so a caller passinginitial_contributions=[SB,BB]+initial_pot=0(intending to declare posted blinds) producedcs=[0,0]andpot_total=0at every leaf — collapsing the preflop Nash to fold-everywhere. This was HARD-BLOCKING the Premium-A blueprint.What changed
PreflopRvrState::initial(crates/cfr_core/src/preflop_rvr.rs:229-251) honorsconfig.initial_contributions:contribution[i] = max(blind_amount[i], initial_contributions[i]). Both well-formed config shapes —[0,0]+pot=0(engine posts blinds) and[SB+ante,BB+ante]+pot=SB+BB+2*ante(caller declared blinds as dead money) — produce Nash-equivalent strategies (leaf payoffs differ by exact per-player constants).HUNLState::initial_preflop(crates/cfr_core/src/hunl.rs:382-414) same fix. The postflop branch athunl.rs:318was already correct and is untouched.HUNLConfig::validate()added incrates/cfr_core/src/hunl.rs. Mirrors Python's__post_init__guard atpoker_solver/hunl.py:124-176, plus accepts the caller-declared-blinds preflop shape. Defense-in-depth perfeedback_silent_skip_hazard— PyO3 callers deserialize straight intoHUNLConfigand skip Python's__post_init__.validate()intosolve_hunl_preflop_rvrso the bug-triggering config ([50,100]+pot=0) HARD-FAILs at the entry point instead of silently producing a fold-everywhere equilibrium.Tests added
crates/cfr_core/tests/preflop_initial_contributions.rs— 11 new Rust tests covering AA-does-not-fold regression, identical-strategies diff-test, sixHUNLConfig::validate()accept/reject cases, dead-money subgame acceptance, and entry-point rejection.tests/test_preflop_rvr_diff.py::test_engine_agnostic_to_blind_declaration— Python-side parametrized variant of the diff-test, exercising the PyO3 boundary.Verification
preflop_rvr_smoke::aa_vs_kk_closed_form_aa_does_not_fold) still PASSes — no regression on the engine-posts-blinds path.test_preflop_rvr_diff.pyPython diff tests PASS (test_pushfold_equivalence_15bb,test_aa_only_vs_kk_only_closed_form,test_engine_agnostic_to_blind_declaration,test_aggregator_drift_premium_ranges).cargo test -p cfr_core --lib: 75 unit tests PASS.cargo clippy --lib --tests -p cfr_core -- -D warnings: clean.ruff check tests/test_preflop_rvr_diff.py: clean.Diff-test result
engine_posts_vs_caller_declared_blinds_produces_identical_strategies: max_drift < 1e-4 between[0,0]+pot=0and[SB+ante,BB+ante]+pot=SB+BB+2*ante(PASS). The engine is now agnostic to which way blinds are declared.Constraints respected
hunl.rs:318untouched (was already correct).State::initialand the newvalidate().cfr_corebenchmark formatting issues (benches/preflop_rvr_profile.rs,benches/rvr_profile.rs) are not in this PR's scope.🤖 Generated with Claude Code