Fix AnalysePlay under-counting (issue #156): reuse solver context across cards#157
Conversation
analyse_later_board constructed a fresh SolverContext for every card, so each incremental hint-bounded search ran against a cold, empty transposition table. The per-card hint/hintDir bounds assume the warm TT built by the initial solve and the preceding cards; with a cold TT the null-window search settles on the wrong bound and under-counts tricks. This is the same defect that was fixed for calc_dd_table in 27030ba ("reuse SolverContext across declarers") -- the analyse-play path was simply missed. Make analyse_later_board take SolverContext& and reuse the caller's context (and its TT), exactly as solve_same_board already does. AnalysePlayBin passes its persistent outer_ctx to every call. ini_depth still resolves to the shared ThreadData, so the per-card decrement is unchanged; only the TT now persists across the play, as it did before the context refactor. Add a self-consistency regression test: AnalysePlayPBN's trick count after each card must equal an independent SolveBoardPBN of that same position (no external reference solver needed). Covers the issue dds-bridge#156 deal plus deterministic random play-outs. The test fails on the old code and passes with the fix. Fixes dds-bridge#156 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes issue #156 where AnalysePlay under-counts tricks. After the recent solver context refactor, analyse_later_board was constructing a fresh SolverContext per card (ctxLater), giving each call a cold transposition table. Since the per-card hint-bounded null-window search depends on a warm TT built up by the initial solve and preceding cards, the searches settled on incorrect bounds. The fix takes SolverContext& as a parameter and reuses the caller's persistent context, mirroring the earlier calc_dd_table fix (commit 27030ba). A new self-consistency test compares AnalysePlayPBN results against fresh SolveBoardPBN calls at every ply.
Changes:
- Change
analyse_later_boardsignature to acceptSolverContext&instead ofstd::shared_ptr<ThreadData>&, replacing all internalctxLateruses with the passed-inctx. - Update
AnalysePlayBinto pass its persistentouter_ctx(renamed-purpose) intoanalyse_later_boardrather than creating per-call contexts. - Add
analyse_play_consistency.cpptest (and Bazel target) verifying AnalysePlay matches SolveBoard for the issue #156 deal and deterministic random play-outs.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| library/src/solver_if.hpp | Changes analyse_later_board declaration to take SolverContext&. |
| library/src/solver_if.cpp | Replaces per-call ctxLater with caller-supplied ctx, preserving warm TT state across cards. |
| library/src/play_analyser.cpp | Removes local thrp, passes the persistent ctx to analyse_later_board. |
| library/tests/solve_board/analyse_play_consistency.cpp | New self-consistency regression test covering the issue #156 deal and random play-outs. |
| library/tests/solve_board/BUILD.bazel | Adds Bazel target for the new test. |
zzcgumn
left a comment
There was a problem hiding this comment.
Thanks for this. Changes look good to me.
Summary
AnalysePlayunder-counts tricks for many deals (issue #156). The root cause is in the incremental analyser, not the alpha-beta core — a largeSolveBoard/CalcDDtabledifferential against 2.9 shows zero differences; only the analyse-play path regressed, which is whydtest/list10000.txtdoes not catch it.AnalysePlayBinsolves the opening position once, then for each later card callsanalyse_later_board, which runs a hint-bounded null-window search. That search is only correct against the transposition table built by the initial solve and the preceding cards — the per-cardhint/hintDirbounds assume a warm TT.After the context refactor,
analyse_later_boardbuilt a freshSolverContextper call:SolverContext ctxLater{thrp};Each
SolverContextowns its own TT (SearchContext::tt_, created lazily), so every card got a cold, empty TT. The null-window search then settles on the wrong bound and under-counts. The error is intermittent and "self-heals" (e.g. in #156: wrong at plies 2, 4, 7–15, correct again from 16), because some positions resolve correctly within the bounds regardless of TT state.This is the same defect already fixed for
calc_dd_tablein 27030ba ("Fix calc DD table consistency by reusing SolverContext across declarers").analyse_later_boardis the play-analyser analogue and was missed by that fix.Fix
Make
analyse_later_boardtakeSolverContext&and reuse the caller's context (and its warm TT), exactly assolve_same_boardalready does.AnalysePlayBinpasses its persistentouter_ctxto every call.ctx.search().ini_depth()still resolves tothr_->iniDepth(sharedThreadData), so the per-card--ini_depthis unchanged; the only behavioral difference is that the TT now persists across the play, as it did before the refactor.Test
Adds
library/tests/solve_board/analyse_play_consistency.cpp: an independent self-consistency check that needs no external reference solver —AnalysePlayPBN's trick count after each card must equal a freshSolveBoardPBNof that same position. It covers the exact issue #156 deal plus deterministic random play-outs.Verification
developdiverges on 336/500; with this fix, 0/500.Fixes #156