The win-time bonus is Ferrous house-rule scoring policy, not a bridge to
the upstream klondike crate, so it does not belong in klondike_adapter.
Relocate it to a dedicated solitaire_core::scoring module and update the
sole caller (win_summary_plugin) and the adapter/settings doc references.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
card_to_id was a frankenstein 0..=51 id shim. Replace it with card_game::Card:
- feedback_anim deal jitter now seeds off a hash of the Card itself
- radial_menu RightClickRadialState.cards: Vec<u32> -> Vec<Card>
- wasm CardSnapshot.id: u32 -> Card (serialises transparently as a plain JS
number, the same opaque key the renderer already used; new test asserts the
JSON id field is a number)
- wasm DebugInvariantReport deck-completeness check reworked from a [bool;52]
index into a HashSet<Card> + Card::new reference deck; the out-of-range check
is dropped since a Card is always valid
Delete card.rs entirely: the Card/Deck/Rank/Suit re-exports move to the crate
root and the 69 `solitaire_core::card::` import paths flatten to `solitaire_core::`.
The JS card.id is purely an opaque identity key (Map key / dataset.cardId, no
arithmetic, card faces render from rank+suit), so the value change is safe.
cargo test --workspace and clippy --workspace --all-targets -- -D warnings green.
Closes#83
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DrawMode was a 1:1 mirror of klondike::DrawStockConfig (DrawOne/DrawThree).
Delete it and use the upstream type everywhere; re-export DrawStockConfig from
solitaire_core. config_for assigns draw_stock directly and draw_mode() returns
session.config().inner.draw_stock.
Serde is unchanged — DrawStockConfig serialises to the same "DrawOne"/"DrawThree"
named variants, so persisted game_state.json / replay JSON stay byte-compatible
(no migration). Field/method/variable names containing draw_mode are unchanged.
35 files, mechanical type swap across all crates. Implemented via a multi-agent
workflow (core → per-crate consumers → verify). cargo test --workspace and
clippy --workspace --all-targets -- -D warnings green.
Closes#82
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the standalone solver wrapper module. Its thin shaping — build a
solve-budgeted Session, run card_game::Session::solve(), extract the first
useful move — moves onto the domain type in solitaire_core as
GameState::solve_first_move() / GameState::solve_fresh_deal(), with the budget
consts and the SolveOutcome alias re-exported from solitaire_core.
Solving is deterministic, IO-free game logic, so core (which already owns
GameState and exposes session().solve()) is its correct home; solitaire_data is
the persistence/sync layer and never should have owned it.
Consumers now call the core API directly:
- engine: pending_hint (solve_first_move), game_plugin + play_by_seed_plugin
(solve_fresh_deal), input_plugin (budget consts)
- assetgen: gen_seeds + gen_difficulty_seeds (solve_fresh_deal)
The solver tests move to solitaire_core. cargo test --workspace and
clippy --workspace --all-targets -- -D warnings both green.
Resolves the "delete the solver" directive — card_game provides the solver.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the (from, to, count) tuple as an internal move-passing wrapper.
Game logic now stays in KlondikeInstruction space end to end:
- Add GameState::apply_instruction, the native apply path. move_cards
becomes a thin pile-coordinate adapter that converts to an instruction
and delegates, so move bookkeeping (validation, score/recycle history,
undo snapshot) lives in one place instead of being duplicated.
- next_auto_complete_move matches DstFoundation directly instead of
projecting every candidate to pile coordinates.
- proptests and the storage round-trip test apply instructions directly
rather than round-tripping instruction -> tuple -> move_cards.
The single instruction -> pile decode is renamed instruction_to_highlight
-> instruction_to_piles and kept in core: decoding a tableau run length
needs upstream pile-stack types core does not re-export, so relocating it
would duplicate the logic across engine and wasm. The two rendering edges
(engine hint highlight, wasm debug move list) call this one decoder; the
engine's hint_piles is a thin delegation to it.
Also includes the CardEntityIndex render-side index and a SelectionPlugin
init_resource fix so update_selection_highlight no longer panics in test
harnesses that omit CardPlugin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Rhys: card_game's solver is the real engine, so drop the redundant
adapter types in solitaire_data::solver rather than maintain a parallel
verdict/config/move vocabulary.
- Delete SolverResult, SolverConfig, SolverMove, and snapshot_to_solver_move.
The verdict now reads straight off card_game's return:
Ok(Some(instr)) = winnable (first move on the path)
Ok(None) = provably unwinnable
Err(_) = inconclusive (budget exceeded)
- SolveOutcome is now Result<Option<KlondikeInstruction>, SolveError>.
- try_solve / try_solve_from_state take plain (moves_budget, states_budget)
u64s; add DEFAULT_SOLVE_{MOVES,STATES}_BUDGET consts.
- snapshot_to_solver_move duplicated core's GameState::instruction_to_move,
so make that pub and have the hint convert the first-move instruction to
highlighted (from, to) piles through it. Re-export KlondikeInstruction
from solitaire_core.
- HintSolverConfig now holds { moves_budget, states_budget } instead of
wrapping the deleted SolverConfig.
- Update consumers: pending_hint, play_by_seed (verdict badge), game_plugin
(choose_winnable_seed), input_plugin, hud_plugin, and the gen_seeds /
gen_difficulty_seeds asset tools.
solver.rs drops 274 -> 140 lines. cargo test --workspace and
cargo clippy --workspace --all-targets -- -D warnings pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finish the half-applied Card refactor. solitaire_core::card::Card is now an
alias for the opaque card_game::Card: suit()/rank() are methods, there is no
id or face_up field, and it is Clone+Eq+Hash but not Copy. Pile accessors
return Vec<(Card, bool)> where the bool is face-up.
Card identity is now the Card value itself (via Eq/Hash), not a numeric u32:
- CardEntity stores `card: Card` (was `card_id: u32`); lookups compare cards.
- Drag/selection collections and the touch/keyboard selection setters use
Vec<Card>; CardFlippedEvent/CardFaceRevealedEvent/HintVisualEvent carry Card.
- replay_overlay and feedback/settle/deal animations updated accordingly.
solitaire_wasm: CardSnapshot derives its JSON id from suit+rank (matching the
desktop engine), and consumes the (Card, bool) pile tuples.
test-support: TestPileState tableau overrides now carry a per-card face-up flag
so tests can place face-down tableau cards. set_test_tableau_cards keeps its
Vec<Card> signature (defaulting to face-up); new set_test_tableau_cards_with_face
takes Vec<(Card, bool)>.
cargo test --workspace passes (engine lib 897 ok, 0 failed); cargo clippy
--workspace --all-targets -- -D warnings is clean. Save/serde format unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Delete solitaire_core::solver — moved wholesale to solitaire_data::solver (re-exported at crate root)
- Delete solitaire_core::pile — no external users
- Move DrawMode from game_state to klondike_adapter; re-export as solitaire_core::DrawMode
- Remove schema_version field from GameState (redundant — deserializer stamps it from the constant)
- Update all callers across solitaire_data, solitaire_engine, solitaire_assetgen, solitaire_wasm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- solitaire_data: add game_state_v3_mid_game_round_trip — first test to
exercise the schema-v3 instruction-replay path with a real mid-game
state (draws + card move + undo); GameState::PartialEq validates all
pile layouts, score, move_count, undo_count, and recycle_count
- solitaire_data: add save_format_v2_is_rejected — schema-version gate
test, parallel to the existing v1 rejection fixture
- solitaire_core: add SavedInstruction proptest (256 random cases across
all three instruction variants) and four boundary unit tests for
out-of-range Tableau/Foundation/SkipCards values
- solitaire_core: document pile() KlondikePile::Stock → waste mapping
- solitaire_core: document replay_config() take_from_foundation=true
invariant and the re-export policy for upstream types
- Cargo.toml: pin card_game + klondike git deps to rev 99b49e62
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire card_game 0.4.0 and klondike 0.3.0 as workspace deps in
solitaire_core and clean the integration seam across five areas:
- Move From<card_game::Suit/Rank> bridge impls out of card.rs and into
klondike_adapter.rs so the product-type module is upstream-dep-free
- Add `use crate::card` alias to adapter; rename card_from_kl parameter
to avoid shadowing; correct score_for_undo doc (it is Ferrous policy,
not an upstream default — the solver explicitly passes undo_penalty=0)
- Mark Pile as a read-only projection / data-transfer type in its doc
comment so game logic isn't accidentally routed through it
- Add GameState::session() read accessor exposing the underlying
Session<Klondike> for replay history and solver use by external crates;
update solver.rs to use the accessor instead of the pub(crate) field
- Re-export Foundation, Klondike, KlondikePile, Session, Tableau from
solitaire_core::lib so downstream crates (engine, wasm) can import
from one place without a direct klondike/card_game dep
- Add proptest property tests: card conservation (52 unique IDs always
present), deal determinism, undo pile-layout invariant, legal moves
always succeed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step 1 — Cargo & registry:
- Add .cargo/config.toml with Quaternions sparse registry
(https://git.aleshym.co/api/packages/Quaternions/cargo/)
- Add klondike = "0.2.0" to workspace deps (+ card_game v0.3.0,
arrayvec v0.7.6 as transitives via the Quaternions registry)
- Add klondike as a solitaire_core dep
Step 3 — KlondikeConfig / MoveFromFoundationConfig:
- KlondikeAdapter::new(draw_mode, take_from_foundation) builds a
KlondikeConfig with the correct DrawStockConfig and
MoveFromFoundationConfig (Allowed/Disallowed); exposes it via
klondike_config() for future solver and pile-mapping steps
Step 4 — Scoring via ScoringConfig:
- GameState.adapter (serde(skip)) owns the authoritative KlondikeConfig
with ScoringConfig::DEFAULT (WXP values)
- score_for_move/flip/undo/recycle replace direct scoring.rs calls;
scoring.rs retained for reference and future deletion
- score_for_recycle implements the WXP free-recycle allowance rule
that ScoringConfig::recycle cannot express (flat delta)
- PartialEq/Eq for KlondikeAdapter compare draw_stock and
move_from_foundation only (scoring is always DEFAULT)
All 192 solitaire_core tests pass; clippy -D warnings clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes Quat investigation #1. Today some Klondike deals are
unwinnable from the start and the player has no signal that the
deal they were given is solvable. A new Settings → Gameplay toggle
"Winnable deals only" (default off) makes the engine retry seeds
at deal-time until the solver returns Winnable, up to a cap.
Solver
solitaire_core::solver is a hand-rolled iterative-DFS solver with
memoisation on a 64-bit canonical state hash. Move enumeration is
priority-ordered: foundation moves first (zero choice when an Ace
or rank-up exists), inter-tableau moves second, waste-to-tableau
third, stock-draw last. The draw is skipped when the cycle counter
shows we've recirculated the entire stock without progress —
Klondike's deterministic stock cycle means further draws can't
unlock anything new.
Two budget knobs (move_budget = 100k, state_budget = 200k by
default) cap pathological cases at Inconclusive; the caller treats
Inconclusive as "winnable" so the player isn't penalised for the
solver giving up. Median solve time is 2 ms; pathological
inconclusives top out near 120 ms.
Switched from recursive to iterative DFS after a real-deal solve
overflowed Rust's default 8 MB thread stack. Behaviour identical;
the change is invisible to callers.
Pure logic — solitaire_core has no Bevy or I/O. Same input always
yields the same SolverResult.
Settings
Settings.winnable_deals_only is a #[serde(default)] bool; legacy
files load to false. SOLVER_DEAL_RETRY_CAP = 50 caps the retry
loop. The Settings → Gameplay toggle reads as "Winnable deals only"
with a "(may take a moment when on)" caption.
Engine integration
handle_new_game's seed-selection path now branches on the toggle.
When on AND mode is Classic AND no specific seed was requested
(daily challenges, replays, and explicit-seed requests bypass the
solver), choose_winnable_seed walks seed N, N+1, N+2, … calling
try_solve until it finds Winnable or Inconclusive. If the cap is
hit without a verdict, the latest tried seed is used so the player
always gets a deal rather than spinning forever.
19 new tests (11 solver, 3 settings, 5 engine including the
choose_winnable_seed unit). Two ignored bench/scan helpers
(solver_bench, find_unwinnable) for ad-hoc profiling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces AchievementContext (stats + last-win snapshot), AchievementDef,
ALL_ACHIEVEMENTS, and check_achievements. Adds undo_count to GameState
so the no_undo and speed_and_skill conditions are evaluable.
Skipped achievements that depend on features not yet built:
daily_devotee (progress), comeback (recycle counter), zen_winner (modes),
perfectionist (max-score calc). They land in later phases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements PileType/Pile, MoveError (thiserror), Deck with seeded shuffle,
deal_klondike layout, foundation/tableau placement rules, and Windows XP
Standard scoring — 41 tests, clippy clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>