Skip to content

Klondike: weight cleanup and a Draw 3 stock-alignment term in the IDA* heuristic - #4

Merged
jsgrrchg merged 3 commits into
mainfrom
zeron/weights-rs-optimization-review
Oct 15, 2026
Merged

jsgrrchg merged 3 commits into
mainfrom
zeron/weights-rs-optimization-review

Conversation

@jsgrrchg

Copy link
Copy Markdown
Owner

Summary

Three commits on the Klondike weights: two behaviour-preserving refactors that make weights.rs say what the solver actually does, and one new heuristic term for Draw 3 that charges the next foundation cards the current stock pass cannot reach. No change to the FFI surface or to Draw 1 results.

Draw 3 Draw 1
main (64f4c9a), 500 games unseeded (12 Oct) 70.00%, 5.70 s 85.40%, 9.38 s
This branch, 500 games unseeded (14 Oct) 75.20%, 7.49 s unchanged by construction
Thoughtful-Klondike ceiling (Solvitaire) 81.945% ± 0.084% ≈ 90.4%
Commit Change Effect
d4f1197 Drop inert weights and stale heuristic comments None (ordering keys pinned by a test)
516656b Checkpoint adoption thresholds expressed by foundation count None expected
bb896ae MISALIGNED_TARGET_COST in the Draw 3 heuristic Draw 3 +5.2 points, time +31% (see open issues)

Changes

Drop inert weights and stale heuristic comments (d4f1197)

A review of weights.rs against its two consumers found constants that could not change any result:

  • DEADLOCK_PENALTY had no references.
  • PRIORITY_EMPTY_COL_BONUS, PRIORITY_STOCK_ADVANCE and PRIORITY_STOCK_RECYCLE were constant offsets inside a bucket of successor_bucket that holds only moves of their own kind, so they never reordered anything. Pure stock advances are never generated since the stock macros, and there is at most one recycle per node. PRIORITY_SAFE_FOUNDATION stays: a safe foundation move that reveals a card competes inside the reveal bucket.

local_secondary_priority lost those branches and its from_board parameter. A new test, single_kind_buckets_carry_no_base_secondary_priority, pins the ordering keys of an emptying move and a recycle.

The heuristic comments claimed admissibility. They were wrong: the depth, blocked-King, waste and imbalance terms all overestimate the remaining move count, and the solver gave up optimality long ago with checkpoints and node limits. The comments now say the heuristic is informative, which removes a false constraint on tuning. heuristic_cost is clamped at zero (it could go negative with four empty columns and an almost complete foundation), the HIDDEN_DEPTH_DIVISOR formula comment matches the code, and two redundant casts are gone.

Checkpoint adoption thresholds by foundation count (516656b)

is_adoptable required a progress delta of 3% of |start_score|, clamped per phase. Since progress_score is dominated by 1300 points per foundation card, that was about 39 points per card, and only one end of each clamp was ever reached: 150 early, 1000 mid, 500 late.

ADOPT_RELATIVE_PCT and the three clamp pairs are replaced by ADOPT_PER_FOUNDATION_CARD = 39, ADOPT_EARLY_MIN = 150, ADOPT_MID_MAX = 1000 and ADOPT_LATE = 500, computed by CheckpointPolicy::adopt_threshold(fc). The start score no longer affects the threshold. The only behavioural difference is that the non-foundation signals in the score (face-up, hidden, autoplay bonus) no longer shift the threshold, by at most ~150 points mid-game. Tests pin the threshold at every phase edge.

This makes the threshold slope and the 1000 → 500 drop at 30 foundation cards explicit and tunable; neither was visible before.

Skipped stock targets in the Draw 3 heuristic (bb896ae)

Draw 3 shared every weight with Draw 1 except the node-limit table. The one Draw 3-specific signal, STOCK_MISALIGNED_TARGET_PENALTY, only entered progress_score (checkpoint choice), never heuristic_cost (IDA* pruning). The search pruned a state whose next target was reachable this pass exactly like one that needed a full recycle.

BoardEval::from_board_fast now counts misaligned_targets: next foundation cards in the remaining stock whose offset from the pile card is not a multiple of the draw size, excluding the last stock card, which is always exposed at the end of a pass. heuristic_cost adds MISALIGNED_TARGET_COST = 2 per such card while hidden cards remain. The scan is at most 24 cards and runs only for draw > 1, so Draw 1 is unchanged by construction. Three tests cover the count against draw 1, the alignment shift when stock_index changes, and the endgame branch, which ignores the term.

First 500-game run: 75.20% (was 70.00%), with 251 games solved without a checkpoint (211 before) and 0.35 checkpoints per won game (0.43 before). Notes and caveats are in docs/results/KlondikeDraw3_results_20261014_220504.csv.

Open issues

  • Time. Average duration rose 31% in Draw 3, and ~47% in stalled games with checkpoints, which spend the full node budget. The 24-card scan cannot explain that alone; the search dynamics or machine load may. A seeded pair with MISALIGNED_TARGET_COST at 0 vs 2 on this build isolates the term from its time cost.
  • Not yet swept. MISALIGNED_TARGET_COST in {1, 3}; counting waste targets misaligned for the next pass; applying the term in the endgame branch, where a skipped target is what blocks a Draw 3 win.
  • The Draw 3 benchmark in this PR is unseeded and not pairable game by game with the 12 Oct run.

Testing

cargo test in solver-core-rs. Benchmarks via solverlab-tui --headless --game klondike3 --sims 500 --parallel 10.

Remove DEADLOCK_PENALTY (unused) and the three secondary priorities that
only ever applied as a constant offset inside their own ordering bucket:
PRIORITY_EMPTY_COL_BONUS, PRIORITY_STOCK_ADVANCE and PRIORITY_STOCK_RECYCLE.
The successor order is unchanged; a new test pins the ordering keys of the
single-kind buckets.

Rewrite the heuristic comments: heuristic_cost is informative, not
admissible, and the weights may be tuned for search speed. Clamp the
heuristic at zero, fix the HIDDEN_DEPTH_DIVISOR formula comment and drop
two redundant casts.
…ion count

The adoption threshold was 3% of |start_score| clamped per phase. Since
progress_score is dominated by 1300 points per foundation card, that was
~39 points per card, and only one end of each clamp was ever reached
(150 early, 1000 mid, 500 late).

Replace ADOPT_RELATIVE_PCT and the clamp pairs with an explicit slope
(ADOPT_PER_FOUNDATION_CARD) and the three reachable bounds, computed by
CheckpointPolicy::adopt_threshold(fc). The start score no longer affects
the threshold; the non-foundation signals it carried shifted it by at
most ~150 points mid-game. Tests pin the threshold at each phase edge.
BoardEval::from_board_fast now counts the next foundation cards sitting
in the remaining stock at a position the current pass skips: offset from
the pile card not a multiple of the draw size, last card excluded since
it is always exposed at the end of the pass. heuristic_cost adds
MISALIGNED_TARGET_COST = 2 per such card while hidden cards remain. The
scan is at most 24 cards and only runs for draw > 1, so Draw 1 is
unchanged by construction.

First unseeded Draw 3 run: 75.20% (was 70.00%), with more games solved
in the first chunk, but average duration rose 31%. A seeded pair with
the cost at 0 vs 2 is still needed to isolate the term; results and
caveats are in docs/results/KlondikeDraw3_results_20261014_220504.csv.
@jsgrrchg
jsgrrchg merged commit 7490538 into main Oct 15, 2026
14 checks passed
@jsgrrchg
jsgrrchg deleted the zeron/weights-rs-optimization-review branch October 15, 2026 02:34
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