apply_safe_area_to_modal_scrims now sets both padding.top (status-bar
height) and padding.bottom (gesture-bar height) on every ModalScrim.
With align_items/justify_content: Center on the scrim, the modal card
lands at the visual midpoint of the visible area between the two system
bars, fixing the slight upward shift that occurred when only the bottom
inset was applied.
Also: mark all rewrite-plan phases (0–3) complete; drop obsolete stash
whose 20 files are already incorporated into master; update CLAUDE.md
§14.3 to document both edges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 3 of the in-place card_game rewrite.
Two bugs on undo:
1. recycle_count was incremented when recycling but never decremented on
undo, causing the free-recycle allowance to be exhausted faster than
it should be after undo+redo cycles.
2. undoing a penalised recycle applied the −15 undo penalty on top of
the post-penalty (post-recycle) score rather than on the pre-recycle
score, compounding the −100 / −20 penalty rather than reversing it.
Fix:
- Add score_history: Vec<i32> and is_recycle_history: Vec<bool> to
GameState, both parallel to session.history() at all times.
- Extract pre_instruction_score_delta() helper — single source of truth
for all scoring logic, called from draw(), move_cards(), and the
Deserialize replay.
- draw() and move_cards() push to both stacks before processing.
- undo() pops from both stacks: uses the popped pre-move score as the
base for apply_undo_score() and decrements recycle_count if the
undone instruction was a recycle.
- Deserialize rebuilds is_recycle_history and recycle_count from the
instruction replay (recycle detection needs only pre-instruction
session state, so it is always correct across save/load cycles).
score_history is not rebuilt on load (undo-penalty history is absent
from saved_moves); undo falls back to old behaviour for pre-load
moves, but is fully correct for moves made in the current session.
- Remove recycle_count from PersistedGameStateIn (now rebuilt; serde
silently ignores the field in existing JSON saves).
Tests added:
- recycle_count_decrements_when_recycle_is_undone
- score_recycle_penalty_is_reversed_on_undo
All 71 solitaire_core tests and full-workspace suite pass; clippy clean.
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>
Three independent hardening changes:
1. bcrypt on a blocking thread: hash() and verify() are CPU-bound
(~300 ms at cost 12). Running them directly on an async task starved
the Tokio runtime under concurrent load. Wrapped in spawn_blocking.
2. Async avatar file I/O: std::fs::write/rename/remove_file in an async
handler blocks the executor. Replaced with tokio::fs equivalents.
3. JWT_SECRET minimum length: a secret shorter than 32 bytes is fatally
weak. validate_jwt_secret() now rejects it at startup with a clear
message rather than silently accepting it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the 2026-05-09 seed lists with seeds regenerated on 2026-06-04.
All seeds remain verified winnable within their respective solver budgets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build_android_apk.sh no longer requires all four env vars to be set
manually. It probes common SDK paths and uses the newest installed
build-tools/NDK/platform when vars are absent. Also adds llvm-strip
pass to strip debug symbols from .so files before packaging (controlled
by STRIP_NATIVE_LIBS, default 1), moves the debug keystore to a stable
target/android/debug.keystore path, and prints resolved paths at start.
Also adds scripts/ANDROID_TESTING.md and scripts/android_smoke.sh for
on-device smoke testing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the dependency on bevy::android::ANDROID_APP inside
android_keystore.rs. Instead, solitaire_data owns a process-wide
OnceLock<JavaVM> initialised by a new pub fn init_android_jvm().
solitaire_app calls it from android_main before run() so JNI is
ready before any auth-token operation can execute.
- android_keystore: drop ANDROID_APP import; add ANDROID_JVM OnceLock
and init_android_jvm(vm_ptr: *mut c_void)
- solitaire_data/lib.rs: re-export init_android_jvm for android target
- auth_tokens.rs: update doc comment (Android backend is now complete)
- solitaire_app/lib.rs: call init_android_jvm from android_main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All downstream crates now import Foundation, KlondikePile, Tableau,
Klondike, Session, Suit, Rank exclusively from solitaire_core.
solitaire_core is the single version-pin point for the upstream crates.
- solitaire_engine: 19 files updated, klondike direct dep removed
- solitaire_wasm: use statement updated, klondike direct dep removed
- solitaire_data: unused klondike dep removed
- Cargo.lock: klondike no longer a direct dep of engine/wasm/data
- Full workspace clippy clean, all tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the parallel solitaire_core::Suit and solitaire_core::Rank
definitions with pub-use re-exports from card_game. card_game upstream
gained serde, is_black(), and value() to make this clean.
- card.rs: remove Suit/Rank enums and impls; add pub use card_game::{Suit,Rank}
- klondike_adapter.rs: remove From<card_game::Suit/Rank> bridges (now same type)
- Simplify card_from_kl: .into() calls become direct assignment
- Cargo.toml: switch to git deps (serde feature), Cargo.lock updated
All 62 solitaire_core tests pass; clippy clean.
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>
solitaire_wasm/src/lib.rs — 5 new unit tests (9 total, was 4):
- serialize_from_saved_round_trip: board key matches after JSON round-trip
- undo_reverts_to_prior_state: state + history length restored after undo
- draw_one_advances_waste_by_one: DrawOne takes exactly 1 card from stock
- draw_three_advances_waste_by_three: DrawThree takes up to 3 cards
- debug_apply_move_json_stock_click: JSON DebugMove path via native method
solitaire_server/e2e/tests/game_behaviors.spec.js — 5 new Playwright tests:
- resume overlay shows when localStorage save exists; seed() returns null
until user interacts (before bootstrap completes a game)
- clicking New Game on overlay clears history and starts fresh (0 moves)
- clicking Resume restores saved move history length exactly
- HUD new-game button resets history to 0 and score to 0
- tab-visibility timer: timer freezes during hidden, resumes when visible
(tests the visibilitychange fix from the 500-game UX audit); uses
page.clock.install() to control setInterval without real-time delay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
play_canvas.spec.js covers the window.__FERROUS_DEBUG__ bridge on the
/play route (five tests): bridge availability + seed param, draw3 URL
param, applyLegalMove/undo round-trip, failureReport schema, and
autonomous autoplay invariant batch across 7 seeds.
All tests drive exclusively through the debug bridge — no DOM selectors,
because the Bevy canvas is a single <canvas> element with no HTML
controls.
Also update SESSION_HANDOFF.md to reflect post-v0.35.1 work (10 commits
since 2026-05-18 handoff), new e2e architecture notes, and HiDPI fix doc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
play.html now loads solitaire_wasm.js alongside the Bevy canvas and
exposes the same window.__FERROUS_DEBUG__ object as /play-classic.
The bridge runs an independent SolitaireGame (WASM logic layer) seeded
from ?seed= / ?draw3= URL params; Bevy renders the visual game in
parallel without coupling.
Methods exposed: seed, state, legalMoves, moveHistory, snapshot,
applyLegalMove, applyMove, draw, undo, serialize, fromSaved, newGame,
failureReport, replayPayload, runAutoplay — matching the /play-classic
contract so the shared Playwright harness targets either route without
modification.
cycle_metrics.js: add --route play-classic|play flag (default
play-classic). Routes to /${route}?seed=N. The resume-overlay clear
step is skipped for /play since the Bevy build uses localStorage-backed
WasmStorage, not a #resume-overlay element.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: fit_canvas_to_parent requests a wgpu surface sized in
physical pixels (CSS pixels × devicePixelRatio). On HiDPI displays
(DPR ≈ 2) the physical size (e.g. 2612×1469) exceeds WebGL2's per-
dimension texture limit of 2048, triggering a wgpu validation panic
that kills the WASM thread immediately on the first window resize.
Fix: add `resolution: WindowResolution::default().with_scale_factor_override(1.0)`
to the primary window so Bevy uses CSS/logical pixels as the surface
dimensions. For a 1306×734 CSS viewport this keeps the framebuffer well
within 2048 regardless of devicePixelRatio.
Also remove the temporary [drag] console logging added in the previous
commit — the panic was causing drag to never run, not a hit-test bug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add warn!/info! calls to start_drag so every click that doesn't produce
a drag emits a console line with the cursor world position, stock/waste
sizes, and per-tableau pile lengths. This lets us see in browser DevTools
whether find_draggable_at is returning None (wrong hit position) or
something earlier in the pipeline is blocking.
Remove once root cause is identified.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OnboardingPlugin previously used PostStartup which fires before the
first Update tick — guaranteeing the onboarding modal and the launch
splash (MOTION_SPLASH_TOTAL_SECS = 1.6 s) overlap for the entire
splash duration. The splash sits at Z_SPLASH (the highest UI z-index),
so the two screens fought visually and the user saw a confusing frozen
composite before the splash faded out.
Fix: move spawn_if_first_run to Update and gate it on
`splashes.is_empty()` (no SplashRoot entity alive). A Local<bool>
ensures the spawn fires at most once per session. Cost: ~one frame of
latency after the splash clears, which is imperceptible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
solitaire_server/e2e/:
- smoke.spec.js: verifies /play-classic loads, exposes window.__FERROUS_DEBUG__
bridge, keyboard parity (Space=draw, U=undo), debug failure report, and
replay payload builder exports schema-v2 moves.
- gameplay_review.spec.js: HUD/controls render check, stock-click + undo
player flow, draw-mode toggle, autonomous play invariant batch, and
cycle-detection regression guard.
- cycle_metrics.js: headless cycle-rate analysis tool; run via
`npm run review:cycles` with configurable policy, game count, and
thresholds. Regression gate baked into package.json scripts.
- playwright.config.js: targets the local server at http://localhost:8080.
- package.json / package-lock.json: @playwright/test 1.60.0.
.gitea/workflows/web-e2e.yml:
- Runs on pushes to solitaire_server/, solitaire_wasm/, solitaire_core/,
or Cargo changes. Starts the server binary, waits for /health, runs
the full Playwright suite, uploads test-results/ on failure.
docs/testing-architecture.md: documents the three-tier test strategy
(unit → Playwright smoke → cycle regression) and the __FERROUS_DEBUG__
bridge contract.
scripts/update_quaternions_deps.sh: helper to bump the Quaternions
registry deps (klondike, card_game) by version and run the full
safety gate including deterministic replay checks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pile_positions[KlondikePile::Stock] stores the waste column position
(col_x(1)). card_plugin renders the face-down deck one column to the
left (col_x(0) = Tableau1 x) via `base.x -= tableau_col_step`.
handle_stock_click and handle_touch_stock_tap were using pile_positions
[Stock] directly, so the click hotspot was on the waste card (right
column) instead of the deck (left column). Result: clicking the
visible face-down deck did nothing, while clicking the waste pile
triggered draw.
Fix: compute deck_pos = Vec2::new(tableau1.x, waste_pos.y) and hit-test
both the deck column AND the waste slot. Accepting waste clicks matches
standard Klondike UX where either card acts as the draw trigger.
Touch tap handler receives the same fix.
Also rebuild canvas_bg.wasm with the corrected engine source and
-O2 optimisation (replacing the previous -Oz that caused grey screen).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Apply cargo fmt to solitaire_engine, solitaire_server formatting.
- solitaire_server/src/lib.rs: add https://analytics.aleshym.co to
script-src, img-src, and connect-src so the analytics beacon loads
without a CSP violation.
- docs and README updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Grey screen fix (canvas_bg.wasm):
- Rebuilt Bevy WASM from refactored solitaire_core that removes the
per-game KlondikeAdapter field from GameState. The old binary was
built with wasm-opt -Oz; the large adapter allocation pattern appears
to trigger an over-aggressive wasm-opt optimisation that corrupts
Bevy's render pipeline, causing a permanent grey screen on /play.
- build_wasm.sh: change wasm-opt -Oz → -O2. Speed-optimised level avoids
the size-focused transforms that miscompile Bevy's deep render stacks.
solitaire_core refactoring:
- game_state.rs: remove adapter: KlondikeAdapter field; use static
KlondikeAdapter::config_for() instead of a per-instance allocation.
Gate test_pile_state behind #[cfg(feature = "test-support")] so
production builds carry no test-only heap state.
Add instruction_history() public accessor (delegates to saved_moves()).
- card.rs: add Card::new(), face_up(), face_down() const constructors
for more ergonomic test and wasm code.
- pile.rs, solver.rs: cargo fmt.
solitaire_wasm interactive API:
- lib.rs: add SolitaireGame wasm-bindgen struct with draw(), move_cards(),
undo(), auto_complete_step(), serialize(), from_saved() — the full
player-action surface used by game.js.
Add DebugSnapshot, DebugMove, DebugInvariantReport structs and
debug_snapshot(), debug_legal_moves(), debug_apply_move_json()
methods for e2e test automation (window.__FERROUS_DEBUG__ bridge).
Add replay_moves() to export the current game as a Replay v2 payload.
- solitaire_wasm.js + solitaire_wasm_bg.wasm: rebuilt with new API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile:
- Drop --mount=type=secret,id=cargo_token: the Quaternions private
registry has been migrated to the public Cargo.io path so the build
secret is no longer needed. Removes the requirement for CI_TOKEN to
carry registry credentials.
CI workflow (docker-build.yml):
- Add solitaire_wasm/** and solitaire_web/** to the push-trigger paths
so changes to either WASM crate actually fire the build job.
- Add wasm drift check for solitaire_wasm artifacts (solitaire_wasm.js,
solitaire_wasm_bg.wasm) — exits 1 if solitaire_wasm/ or solitaire_core/
changed without updating the committed pkg files.
- Add hard canvas drift check: solitaire_web/ changes MUST update
canvas_bg.wasm or the deploy gets a stale Bevy binary.
- Add advisory notice for solitaire_engine/ / solitaire_core/ changes
that omit a canvas_bg.wasm rebuild (non-blocking; formatting commits
should not fail CI).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Web client (game.js):
- Restart game timer after undo exits auto-complete sequence
- Pause timer while browser tab is hidden (visibilitychange)
- Validate URL seed — NaN / negative falls back to randomSeed()
- Guard onBoardClick/onBoardDblClick during win (snap.is_won)
- Delay win overlay 320 ms so last card CSS transition finishes
- Force reflow in flashIllegal() to restart shake on rapid re-trigger
Android (safe_area.rs):
- Preserve last-known insets on app resume instead of zeroing them;
eliminates double layout flash on every foreground cycle
All clients — Bevy engine:
- Radial menu: clamp icon anchors to viewport bounds so icons are
never placed off-screen on narrow phones
- Auto-complete: deactivate state.active when is_auto_completable
goes false (undo mid-sequence) to stop perpetual background retry
- Touch selection: gate highlight rebuild on is_changed() — was
despawning/respawning entities every frame unnecessarily
- Input: fire "Tap a pile to move" InfoToast on first tap in
TapToSelect mode; document cursor_world 1:1 viewport invariant
- Drag threshold: raise desktop from 4 → 6 px to prevent accidental
drags from cursor jitter on HiDPI displays
Desktop / Android (solitaire_app):
- Call cleanup_orphaned_tmp_files() at startup to remove .tmp files
left by crashes between atomic write and rename
Design clarification (klondike_adapter.rs):
- Doc comment: Draw-1 recycling is penalty-only by design (never
blocked) to avoid creating unwinnable positions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'No Moves Available' dialog's New Game button and keyboard shortcut
were firing NewGameRequestEvent::default() (confirmed: false). When the
player has made moves, handle_new_game sees needs_confirm = true, then
hits the scrims.is_empty() guard — which is false because the GameOver-
Screen itself is a ModalScrim — and silently returns without starting a
new game or showing the confirm dialog.
Fix: set confirmed: true in both handle_game_over_input (N/Escape key)
and handle_game_over_button_input (click). The game is already stuck so
the abandon-confirmation guard does not apply, as the doc comment on the
button handler has always said.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cargo fetch --locked was failing with "failed to parse manifest" because
.cargo/config.toml (which registers the Quaternions sparse index) was
never copied into the build image, and the registry's auth token was
never supplied.
Changes:
- COPY .cargo/config.toml into the builder stage so Cargo knows the
Quaternions registry URL.
- Replace bare `cargo fetch` and `cargo build` with
`--mount=type=secret,id=cargo_token` variants that set
CARGO_REGISTRIES_QUATERNIONS_TOKEN from the mounted secret — token
never appears in image layers or docker history.
- Workflow: pass CI_TOKEN as the `cargo_token` build secret.
- Add solitaire_engine/** and solitaire_server/Dockerfile to trigger
paths so engine changes and Dockerfile edits kick off rebuilds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Gate `Startup` and `user_theme_dir` imports in theme/registry.rs
behind `#[cfg(not(target_arch = "wasm32"))]` — they are only used
in the non-wasm code path, eliminating two unused-import warnings
in the WASM release build.
- Rebuild canvas_bg.wasm and solitaire_wasm_bg.wasm with wasm-opt -Oz
(binaryen v129); canvas_bg.wasm drops from 57 MB → 30 MB.
- Add solitaire_web/Cargo.toml stub to server Dockerfile so
`cargo fetch --locked` resolves all workspace members.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this setting, wgpu's naga SPIR-V→GLSL translator uses features
unsupported by ANGLE (Chromium's WebGL2 implementation): storage buffers,
tight inter-stage component limits, etc. ANGLE rejects these shaders with
a fatal "Shader translation error" and a context-lost event.
WgpuSettingsPriority::WebGL2 constrains naga to emit GLES 300es-compatible
GLSL (same limits as WebGL2 spec: no storage buffers, max 31 inter-stage
components, max 255-byte vertex stride). Firefox was already permissive
enough to work without this; Chromium required it.
Result: game renders correctly in both Chromium (ANGLE/SwiftShader) and
Firefox (native WebGL2), with zero JS errors in both environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes found while testing the Bevy WASM build in a real browser:
1. chrono wasmbind: add `wasmbind` feature to workspace chrono dep so
Local::now()/Utc::now() use js-sys::Date on wasm32 (previously
fell through to std::time::SystemTime which panics on wasm32).
2. std::time::SystemTime: replace all remaining direct SystemTime::now()
calls (4 sites across game_plugin, difficulty_plugin, time_attack_plugin,
solitaire_data/storage) with chrono::Utc::now() which is wasm32-safe.
3. user_dir: return empty PathBuf (instead of panicking) when data_dir()
is None on wasm32; there is no filesystem in the browser so user themes
are unsupported and a benign empty path is correct.
4. ThemeRegistryPlugin: gate build_registry_on_startup to non-wasm32
(the filesystem scan for user themes has nothing to scan in the browser;
only the bundled embedded themes are available).
5. AssetMetaCheck::Never: configure AssetPlugin in solitaire_web to skip
`.meta` sidecar fetches — we don't ship .meta files, so the default
AssetMetaCheck::Always produced a 404 flood on every card/background asset.
Result: `http://localhost:<port>/play` boots in Firefox with zero errors
and renders the full Bevy game — home screen, onboarding modal, HUD all
visible. Assets load correctly from /assets/. Chromium has a separate
wgpu-27/ANGLE/GLES shader translation bug (not in our code); Firefox works.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace PileType with typed KlondikePile (Foundation/Tableau variants)
throughout solitaire_core, solitaire_wasm, and solitaire_engine;
ReplayMove now uses SavedKlondikePile for serialisation stability
- Split replay_overlay.rs into replay_overlay/ module (mod, format,
input, update, tests) for maintainability
- Add klondike dep to solitaire_engine and solitaire_data Cargo.toml
- Add TestPileState infrastructure to game_state.rs for engine unit tests
- Rebuild solitaire_wasm pkg (js + wasm artefacts updated)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These were scaffolded for a future KlondikeState::from_piles() path
that never materialised. card_from_kl (used by sync_piles_from_session)
is retained; suit_from_kl / rank_from_kl are narrowed to pub(crate).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Full gap analysis between Quaternions/card_game and solitaire_core,
integration steps 1-7 (all now complete), and references.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes#80: add AUTO_COMPLETE_INITIAL_DELAY (0.75 s) before the first
auto-complete move fires. Previously cooldown was 0.0, causing the
sequence to hijack the board the same frame the condition was met.
Closes#81: fire MoveRejectedEvent in radial_open_on_right_click when
the right-clicked card has no legal destinations, so the shake
animation and invalid-move sound play consistently on desktop/web.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Step 2 prep — card_game dep + type-conversion utilities:
- Add card_game = "0.3.0" (registry Quaternions) to workspace + core
- suit_to_kl / suit_from_kl, rank_to_kl / rank_from_kl
- card_to_kl (drops id, Deck1), card_from_kl (reconstructs stable id
from Clubs-first suit×13+rank ordering matching deck.rs)
- Ready to wire into KlondikeState pile projection once upstream
adds KlondikeState::from_piles()
Step 5 — GameMode-aware scoring in the adapter:
- score_for_move_with_mode, score_for_flip_with_mode (return 0 in Zen)
- apply_undo_score (static, handles Zen + −15 penalty + clamp)
- score_for_recycle_with_mode (return 0 in Zen)
- game_state.rs: all inline GameMode::Zen checks replaced with
adapter calls; adapter is now the single source of truth for
"what score does this action give in this mode"
192 tests pass; clippy -D warnings clean.
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>