Skip to content

Improve Klondike Draw 1/3 progress handling and endgame search - #2

Merged
jsgrrchg merged 6 commits into
mainfrom
zeron/klondike-draw-1-y-2
Oct 6, 2026
Merged

jsgrrchg merged 6 commits into
mainfrom
zeron/klondike-draw-1-y-2

Conversation

@jsgrrchg

@jsgrrchg jsgrrchg commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Klondike could undervalue real reveals, discard useful immediate reversals, and spend search work on equivalent moves or deterministic endgames. This PR corrects those decisions for Draw 1 and Draw 3, reduces unnecessary endgame exploration, and preserves legal partial progress when a full finish exceeds the depth limit.

The latest user-run Draw 1 batch won 408/500 games (81.60%), compared with 396/500 (79.20%) in the original reference. Mean duration was 7.94 s in the latest CSV. These are descriptive results from separate batches, not a paired measurement proving a 2.40 percentage-point improvement. The detailed results and limitations are below.

Changes and expected benefits

P1 — Prioritize actual reveals and preserve foundation-move bonuses

Previously, reveal detection used the change in the total number of face-up tableau cards. That quantity confuses transfers with reveals: moving a visible card to a foundation while flipping the card beneath it can leave the total unchanged, while bringing a card from the waste into the tableau can increase it without revealing anything.

The solver now measures the decrease in face-down cards in the source column for column-to-column and column-to-foundation moves. Other move types do not receive a false reveal bonus. Safe and ordinary foundation moves also retain the reveal, thoughtful-reveal, and critical-path bonuses that their early priority returns previously discarded.

Expected benefit: explore useful reveals earlier and distinguish foundation moves that unlock further progress, using the existing search budget. This changes ordering rather than adding another search pass.

P2 — Correct checkpoint progress evaluation

The near-autoplay bonus now remains active at zero hidden cards. Previously, its contribution dropped from 4,900 with one hidden card to zero with none; it now increases to 6,400. This prevents the final reveal from losing a large progress bonus. Zero hidden cards still does not imply a win when stock or waste remains.

The same-suit dependency penalty now targets a lower hidden card buried beneath a higher hidden card of the same suit. It tracks the highest rank above the current card within each column, caps the rank-gap severity at three, and keeps the existing weight of 160. It is a progress-scoring signal, not proof that a position is unsolvable and not a new hard pruning rule. The alternative of disabling this penalty was not applied.

Expected benefit: rank checkpoint candidates more consistently, especially around the final reveal and blocked foundation dependencies. These corrections do not guarantee that every changed checkpoint choice is better.

P3 — Preserve immediate reversals that can retain a reveal

Moving a card or run away and immediately returning it can leave a newly revealed card face up. The previous inverse-move filter could reject that useful sequence based only on the two move descriptors.

The early filter now permits the relevant column-to-column and column-to-foundation-to-column reversals when the previous source has exactly one visible card and therefore may have revealed one. This is deliberately conservative: one visible card does not prove that a reveal occurred. The search's path signatures still reject actual cycles after applying the move. Definite inverses remain filtered, including cases with zero or multiple visible cards at the previous source.

Expected benefit: retain legal routes that make lasting progress despite moving a card back. The tradeoff is that some ambiguous candidates reach the existing cycle check; configured search limits remain unchanged.

P4 — Reduce endgame and forced-move search overhead

Component Implementation Expected benefit
Direct visible-endgame closure When every tableau card is visible and stock_len == 0, construct a finish from legal primitive foundation moves. Apply this at the root and within recursive search, with a bound based on the number of remaining foundation moves. Avoid exploring rearrangements and repeated bounds when the remaining finish is deterministic.
Equivalent empty destinations Generate one representative genuinely empty destination for tableau, waste, and foundation transfers. Preserve all legal nonempty destinations and their actual column indexes. Reduce duplicate branches caused by interchangeable empty columns.
Safe foundation chains Follow a chain in a loop only while there is exactly one safe successor. Preserve ordinary branching when multiple candidates exist. Avoid a recursive frame per forced step while retaining primitive moves and accounting.

The closure requires no stock and no waste; merely reaching the end of a stock pass is insufficient. It returns an actual replayable move sequence. Primitive depth, node and checkpoint accounting, time checks, and the budget check before recognizing victory are retained. The chain loop also preserves intermediate heuristic bounds, transposition-table checks, cycle detection, progress sampling, and path/signature restoration on backtracking.

The safe-foundation generation path now respects DepthLimit; it previously bypassed that rule. This is a deliberate correctness change: a path that only succeeded by exceeding the declared depth may now stop at that limit.

Follow-up correction — Preserve partial visible-endgame progress

Review identified a regression in the direct closure: if the complete finish exceeded max_depth, it stopped before recording any partial moves, even with allow_partial=true.

Concrete reproduction covered by tests:

  • Valid 52-card position, 40 cards in foundations, no hidden cards, and no stock/waste.
  • Twelve foundation moves remain, but the depth limit is eight.
  • Before this correction: solve(..., true) returned None from the root.
  • Now: it returns eight legal moves, reaching 48 foundation cards. A limit of seven returns seven moves and reaches 47.

The closure now builds only the permitted prefix and records its final progress even between the normal four-visit sampling points. Interrupted prefixes also preserve progress when the node quota is reached. It does not open additional search branches or raise a limit. With allow_partial=false, an insufficient depth still causes immediate pruning without constructing a partial result.

Expected benefit: preserve useful partial output and replayability at search limits. This fixes the API behavior; a better partial result alone does not establish a higher full-game win rate.

Search limits and scope

The configured core limits are unchanged:

Setting Value
Maximum search depth 200
Maximum foundation undos 20
Shared solve timeout 100 s
Maximum checkpoint adoptions 40
Transposition-table entry limit 5,000,000
Checkpoint node limits by existing board category 800,000 / 1,200,000 / 1,800,000 / 2,200,000 / 3,500,000 / 8,000,000 / 800,000

Depth and undo accounting restart from an adopted checkpoint, as before; these are not global caps on the final concatenated path. The node quota is also per search segment, while the timeout is shared across the solve. Preserving these settings does not by itself prove unchanged wall-clock time.

The final implementation includes P1, P2a/P2c, P3, and P4 plus the partial-result correction. It does not include the P5 stock-navigation proposals or the P6 alternative-checkpoint budget split. The FFI signatures and TUI source are unchanged.

Automated validation

The branch adds 25 regression tests: five for P1, three for P2, four for P3, ten for P4, and three for the partial-result correction. Parameterized cases cover more scenarios than the test count alone indicates.

Area Coverage
Reveal ordering Real reveals versus visible transfers; foundation moves retaining reveal and critical-path bonuses; safe-foundation ordering stability.
Checkpoint evaluation Final hidden-card reveal with stock still present; adoptable progress; correct same-suit orientation, severity cap, suit separation, and per-column isolation.
Immediate reversals Single cards and multi-card runs; foundation round trips; retained reveals; rejection of true cycles; early rejection when a reveal is impossible. Full-deck fixtures check card conservation.
Visible closure Legal replay to all four completed foundations; existing prefix included once; already-won boards; exact bound; smallest depth limit; stock, waste, and hidden-card exclusions; dead ends.
Budgets Primitive node accounting, expired deadlines, checkpoint interruption, and budget checks before accepting the last move as a win.
Empty-column symmetry Tableau, waste, and foundation origins; zero, one, and multiple empty columns; all nonempty legal destinations retained.
Forced chains Comparison with a primitive recursive reference across 40 combinations: Draw 1/3 × Fast/Strict × ten scenarios, including intermediate node limits, depth, time, cycles, TT pruning, branching, and heuristic bounds. Snapshots compare paths, signatures, TT contents, counters, progress, and solutions.
Partial-result correction Draw 1/3 × Fast/Strict at depth limits 0, 1, 3, 7, 8, 11, and 12; allow_partial=false; replay and 52-card conservation; an existing prefix; every sampling phase; node limits 1, 3, 7, and 8; expired deadlines.

The new public-API partial-result regression test was run before the correction and failed because the solver returned no legal partial path. It passes after the correction.

Successful validation of the final implementation:

cd solver-core-rs
cargo test --release klondike -- --include-ignored
cargo fmt --check

cd ../solverlab-tui
cargo test --release games::klondike::tests::score_
  • 48 core Klondike tests passed: 42 unit/FFI tests and six integration tests, including the two normally ignored solver/replay smoke tests.
  • Four TUI scoring tests passed. These cover foundation scoring, actual reveal scoring, foundation undo penalties/flooring, and waste-to-tableau moves without a false reveal bonus.
  • Formatting and Git whitespace checks passed.
  • The TUI seeded simulation smoke test was not part of the focused scoring command. The 500-game performance runs below were run by the user.

Lint status: cargo clippy --all-targets -- -D warnings, run inside solver-core-rs, is not clean: it reports 43 unique diagnostics. These include existing core/generated-code issues and too_many_arguments on the primitive-reference test helper introduced for the chain comparison. No new lint was introduced by the subsequent partial-result fix. This PR does not claim a passing standalone core Clippy check.

Recorded Draw 1 simulations

All seven recorded batches have 500 games, eight parallel games, and zero reported timeouts. The original reference CSV already existed on main; this PR adds the six later CSVs with explanatory Spanish comments, preserving their original data rows. Experiment labels below correspond to those comments.

Durations below are recalculated from the per-game rows and rounded to three decimal places. Stalled P95 uses the nearest-rank definition: sorted sample index ceil(0.95 × n) with one-based indexing. This can differ slightly from interpolated percentiles.

Run / source CSV Configuration attribution Wins Win rate Stalled All-game mean Stalled mean Stalled P95
01:02:31 Original reference 396 79.20% 104 7.917 s 9.941 s 13.460 s
01:46:57 P1 396 79.20% 104 8.075 s 9.501 s 12.223 s
02:09:27 P1 + P2a/P2c 392 78.40% 108 7.879 s 9.335 s 11.276 s
02:31:46 Previous changes + P3 393 78.60% 107 7.993 s 9.384 s 11.421 s
13:18:59 Previous changes + P4 402 80.40% 98 7.941 s 9.126 s 11.518 s
14:36:25 Executed revision unconfirmed; retained for completeness 399 79.80% 101 8.170 s 9.778 s 12.883 s
15:27:09 P1–P4 + partial-result correction 408 81.60% 92 7.944 s 9.494 s 11.771 s

What the measurements show

Compared with the original reference, the final batch records 12 more wins out of 500, a 2.40 percentage-point higher win rate, and 12 fewer stalled games. All-game mean duration is about 0.027 s higher; stalled mean is about 0.447 s lower. Stalled P95 falls from 13.460 s to 11.771 s, and the maximum observed stalled duration falls from 16.258 s to 14.028 s.

Compared with the first P4 batch, the final batch records six more wins and virtually the same overall mean, but its stalled mean rises from 9.126 s to 9.494 s and stalled P95 rises from 11.518 s to 11.771 s. The requirement that stalled times must not increase is therefore not yet established by these runs.

The intermediate totals also fluctuate; the data does not demonstrate an independent win-rate improvement for each implementation group.

Limits of the evidence

  • The CSVs do not record a seed, initial deck, or immutable executed revision. Matching game_id values across files cannot be treated as matching deals. Implementation attribution comes from the experiment records/comments, with the 14:36:25 run explicitly left unconfirmed.
  • These are cumulative, separate batches rather than a controlled ablation. The final win-rate difference cannot be attributed specifically to the partial-result fix or any other single change.
  • Each run's stalled group contains its own losing games. A comparison of those group means does not track a fixed set of initially stalled deals.
  • These are Draw 1 performance results. Draw 3 has functional regression coverage here, but no recorded 500-game benchmark in this PR.
  • The CSV configuration fields max_depth=150, undos=-1, and timeout_s=0 are TUI configuration values. The current Klondike adapter constructs KlondikeEngine::new(draw_advance) and does not forward those settings; the effective core defaults are the ones listed above. Per-game elapsed time also includes replay/scoring in that adapter.

The next performance acceptance step is a paired Draw 1 and Draw 3 comparison on the same recorded seeds/decks, at the same parallelism and effective limits. Measure recovered versus lost wins and duration distributions both for all games and for the fixed group that stalled in the reference, in addition to the final stalled group. That is the evidence needed to confirm the intended benefit without longer solve or stalled times.

Review map

Commits: c3ada5f (P1), 06b0cd2 (P2), 546c2ee (P3), 0d47522 (P4), ef495d4 (additional result archive), and eb68d94 (partial-result fix and latest result). The numerical weights/search limits are unchanged; the weights.rs diff updates explanatory comments.

Add ordering regression tests and document the draw-1 trial results for P1a and P1b.
Keep the proximity bonus at zero hidden cards and penalize lower cards buried beneath higher cards of the same suit. Add regression tests and record the P2a and P2c draw-1 trial.
Allow immediate inverses when the previous source may have revealed a hidden card, while keeping path-signature cycle rejection. Add regression coverage and record the P3 draw-1 trial.
Finish visible endgames directly, use one equivalent empty destination, and iterate unique safe foundation chains while preserving primitive accounting. Enforce depth limits on safe foundation moves, add regression tests, and record the P4 draw-1 trial.
@jsgrrchg
jsgrrchg merged commit e7fc850 into main Oct 6, 2026
14 checks passed
@jsgrrchg
jsgrrchg deleted the zeron/klondike-draw-1-y-2 branch October 6, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant