This commit is contained in:
+266
@@ -6,6 +6,272 @@ project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Browser Bevy canvas route and automation support.** Added the `solitaire_web`
|
||||
Bevy WASM build, wired `/play` to the Bevy canvas, added a
|
||||
`window.__FERROUS_DEBUG__` bridge, and introduced Playwright coverage for the
|
||||
web routes and interactive canvas behavior.
|
||||
- **Card-game / klondike integration.** Began replacing in-house card and pile
|
||||
internals with upstream `card_game` / `klondike` types, including adapter
|
||||
work, GameMode-aware scoring, upstream instruction serde, `KlondikePile`
|
||||
migration, and documentation for the in-place rewrite phases.
|
||||
- **Android keystore integration.** Added Android Keystore JNI wiring via
|
||||
`OnceLock` and improved Android token handling around the app directory.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Core type ownership.** Routed all klondike/card imports through
|
||||
`solitaire_core` and unified local `Suit` / `Rank` with upstream `card_game`
|
||||
types.
|
||||
- **Web/WASM build reliability.** Rebuilt WASM packages, cleaned up wasm32 build
|
||||
warnings, added a Binaryen `wasm-opt` pass, pinned upstream git dependencies,
|
||||
and added a CI guard for canvas WASM drift.
|
||||
- **Difficulty seed catalog.** Regenerated the difficulty seed list for the
|
||||
latest verified catalog.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Android and modal safe-area layout.** Modal cards now center within the
|
||||
usable area between status and gesture bars, additional modal-spawn guards were
|
||||
added, and Android build scripts now auto-discover SDK/NDK paths and strip
|
||||
native libraries.
|
||||
- **Core scoring and undo correctness.** Fixed recycle-count drift, undo score
|
||||
compounding, foundation-to-tableau instruction coverage, and several
|
||||
illegal-move paths discovered during the card-game migration.
|
||||
- **Input and rendering issues.** Fixed stock/waste hit testing, accepted waste
|
||||
clicks, delayed first-run onboarding until splash teardown, and kept dragged
|
||||
stacks above all piles.
|
||||
- **Web runtime stability.** Fixed wasm32 runtime panics, HiDPI canvas surface
|
||||
sizing, WebGL2 shader compatibility, and Firefox boot/render behavior.
|
||||
- **Server and data hardening.** Moved bcrypt work to `spawn_blocking`, switched
|
||||
file paths to async I/O where needed, and validated `JWT_SECRET` at startup.
|
||||
- **CI and deployment workflow.** Fixed deploy-branch handling, Docker registry
|
||||
secret usage, and related release automation issues.
|
||||
|
||||
### Tests
|
||||
|
||||
- Added schema-v3 persistence round-trip coverage, foundation-to-tableau
|
||||
instruction coverage, expanded WASM unit tests, and Playwright E2E specs for
|
||||
browser routes and game-canvas behavior.
|
||||
|
||||
## [0.39.0] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **No-legal-moves detection and banner.** Corrected no-move detection across
|
||||
engine, WASM, and web paths, then surfaced the state to players with an
|
||||
in-game banner instead of silently leaving the board stuck.
|
||||
- **Release/deploy automation.** Updated deployment automation so kustomization
|
||||
changes are pushed to the deploy branch instead of the main development
|
||||
branch.
|
||||
|
||||
## [0.38.0] — 2026-05-19
|
||||
|
||||
### Added
|
||||
|
||||
- **Klondike scoring parity.** Added tableau flip bonuses and stock recycle
|
||||
penalties to align scoring with standard Klondike expectations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Core rule enforcement.** Auto-complete now requires an empty waste pile,
|
||||
waste-origin moves reject multi-card transfers, foundation-to-foundation moves
|
||||
are blocked, and undo restores score from the snapshot baseline.
|
||||
- **Modal lifecycle guards.** Added missing `ModalScrim` guards to New Game,
|
||||
restore prompt, and no-moves modal spawn sites.
|
||||
- **Runtime and server robustness.** Tokio runtime setup degrades gracefully
|
||||
instead of panicking; web replay submission casing/date formatting now matches
|
||||
server expectations; avatar routes are publicly reachable when intended.
|
||||
- **Android token and sync merge correctness.** Android tokens are namespaced
|
||||
under the application directory, stored per user, and migrated safely; sync
|
||||
merges preserve draw-one / draw-three win invariants.
|
||||
|
||||
## [0.37.0] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Foundation-to-tableau default.** Made `take_from_foundation` default to true
|
||||
across clients so restored, startup, and web games use the same supported move
|
||||
rules.
|
||||
|
||||
## [0.36.12] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Foundation-to-tableau default.** Set `take_from_foundation` true by default
|
||||
in core so every client inherits the intended house rule without special-case
|
||||
setup.
|
||||
|
||||
## [0.36.11] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Web foundation moves.** Enabled take-from-foundation moves in the web game
|
||||
client.
|
||||
|
||||
## [0.36.10] — 2026-05-19
|
||||
|
||||
### Added
|
||||
|
||||
- **Web resume flow.** Browser games now persist state across page refreshes and
|
||||
can resume through a dialog instead of starting over.
|
||||
|
||||
## [0.36.9] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Settings sync connection flow.** Clicking Connect from Settings now opens the
|
||||
sync-setup modal.
|
||||
|
||||
## [0.36.8] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Restored/startup foundation moves.** Enabled take-from-foundation behavior
|
||||
for restored and startup games, not only newly-created sessions.
|
||||
|
||||
## [0.36.7] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Remaining Android UI issues.** Resolved the final Android UI defects from
|
||||
the review pass, including action-bar/tableau interaction and safe visual
|
||||
spacing.
|
||||
|
||||
## [0.36.6] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Action-bar layout reservation.** Reserved action-bar height in layout so
|
||||
tableau columns do not extend behind bottom controls.
|
||||
|
||||
## [0.36.5] — 2026-05-19
|
||||
|
||||
### Added
|
||||
|
||||
- **Responsive Android action-bar glyphs.** Action-bar glyph font size now scales
|
||||
dynamically on Android to fit available space.
|
||||
|
||||
## [0.36.4] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Classic card labels and HUD overlap.** Corrected classic-card corner-label
|
||||
colors and fixed HUD-band overlap in the Android layout.
|
||||
|
||||
## [0.36.3] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Core, animation, and modal review fixes.** Added the foundation-to-tableau
|
||||
score penalty, hardened solver win validation, guarded zero-duration card
|
||||
animations, aligned initial and dynamic tableau fan spacing, and added missing
|
||||
modal guards for play-by-seed and win-summary paths.
|
||||
- **Pause, messages, credentials, and server validation.** Auto-complete respects
|
||||
pause state, standalone plugins register their events, sync passwords are
|
||||
cleared from ECS buffers after auth task spawn, and avatar MIME validation uses
|
||||
exact matches.
|
||||
- **Foundation pile rendering.** Raised stack fan z-order above corner labels to
|
||||
prevent bleed-through.
|
||||
- **Android release workflow.** Added a manual `workflow_dispatch` trigger to
|
||||
the Android release workflow.
|
||||
|
||||
## [0.36.2] — 2026-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Comprehensive review fixes.** Addressed 26 issues across core rules, replay
|
||||
controls, modal guards, sync payload timing, server replay casing, time-attack
|
||||
overlays, theme refresh, auth overlays, stats ordering, animations, cursor
|
||||
fallbacks, achievements, server temp-file cleanup, and runtime fallback paths.
|
||||
- **Animation and Android label polish.** Cancelled stale win-cascade animations
|
||||
on new game, refreshed Android corner labels on resize, lifted animating cards
|
||||
above lower z-layers, and froze the web timer when auto-complete starts.
|
||||
- **Web package and tooling updates.** Rebuilt the WASM package for
|
||||
foundation-to-tableau moves, added ruflo scaffolding, and ignored ruflo runtime
|
||||
state files.
|
||||
- **Leaderboard test stability.** Made opt-in / opt-out tests robust under
|
||||
parallel test execution.
|
||||
|
||||
## [0.36.1] — 2026-05-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Android HUD gesture conflict.** Stock taps no longer toggle HUD visibility on
|
||||
Android.
|
||||
|
||||
## [0.36.0] — 2026-05-18
|
||||
|
||||
### Changed
|
||||
|
||||
- **Rank model cleanup.** `Rank` now uses explicit discriminants and checked
|
||||
arithmetic, making rank conversions and sequencing more robust.
|
||||
- **Instruction generation.** Refined `possible_instructions` alongside the rank
|
||||
arithmetic cleanup.
|
||||
- **Session handoff.** Recreated `SESSION_HANDOFF.md` to reflect the `0.35.1`
|
||||
state.
|
||||
|
||||
## [0.35.1] — 2026-05-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Leaderboard profile sync.** Fixed three leaderboard/profile issues: wrong
|
||||
toast type for failures, stale display-name label after update, and display
|
||||
name not syncing to the server.
|
||||
|
||||
## [0.35.0] — 2026-05-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Reduced-motion support.** Decorative motion animations are now gated behind
|
||||
`reduce_motion_mode`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Performance and runtime cleanup.** Shared a single Tokio runtime across
|
||||
network tasks and gated frame-hot ECS systems on resource changes.
|
||||
- **Core/data refactors.** Consolidated the application directory name, added
|
||||
`#[must_use]` to pure helpers, derived `Copy` for `DrawMode`, removed
|
||||
redundant clones, added missing derives to `AchievementContext`, and used
|
||||
saturating move-count arithmetic.
|
||||
- **HUD z-layer naming.** Replaced raw HUD popover z-index arithmetic with named
|
||||
layer constants.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Android UI and font safety.** Wired FiraMono to stock-empty labels, removed
|
||||
raw physical safe-area pixels from HUD spawns, replaced unsupported chevrons,
|
||||
corrected the Android help hint label, and fixed touch/drop-zone behavior.
|
||||
- **Engine modal and panic hardening.** Eliminated several runtime panics, added
|
||||
required transforms to modal scrims, constrained dismiss hit-tests, and guarded
|
||||
home overlay respawns.
|
||||
- **Sync/data/server correctness.** Deterministic pile serialization, undo skip
|
||||
handling, byte URL encoding, merge timestamp handling, auth-guarded avatar
|
||||
serving, atomic server writes, and user-id assertions were corrected.
|
||||
- **Display-name and token-file boundaries.** Enforced the 32-character display
|
||||
name limit in the sync client and aligned Android keystore temp-file cleanup
|
||||
with the cleanup glob.
|
||||
- **WASM error reporting.** `state()` and `step()` now return `Result` so errors
|
||||
surface as JavaScript exceptions.
|
||||
- **Sync and leaderboard toasts.** Pull failures and leaderboard opt-in /
|
||||
opt-out failures now produce the intended warning/error feedback.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Corrected stale focus-ring color documentation.
|
||||
|
||||
## [0.34.0] — 2026-05-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Android waste fan and resume layout.** Corrected Android waste-pile fan
|
||||
overlap and a layout desynchronization after resume.
|
||||
- **Card-face artwork.** Fixed the wrong bottom-right suit symbol on the jack,
|
||||
queen, and king of spades.
|
||||
- **Android corner-label font coverage.** Wired FiraMono into Android corner
|
||||
labels and added `CardImageSet` tests to guard the asset path behavior.
|
||||
|
||||
## [0.33.0] — 2026-05-16
|
||||
|
||||
### Fixed
|
||||
|
||||
+30
-38
@@ -1,45 +1,36 @@
|
||||
# Ferrous Solitaire — Session Handoff
|
||||
|
||||
**Last updated:** 2026-06-02 — Web e2e test suite complete; `/play` canvas bridge added and tested. All commits on origin/master.
|
||||
**Last updated:** 2026-06-09 — card_game rewrite follow-up complete; changelog catch-up in progress; analytics follow-up tests added.
|
||||
|
||||
---
|
||||
|
||||
## Current state
|
||||
|
||||
- **HEAD:** `play_canvas.spec.js` added (Playwright tests for `/play` Bevy canvas route)
|
||||
- **Latest tag:** `v0.35.1`
|
||||
- **Working tree:** clean
|
||||
- **Build:** `cargo clippy --workspace -- -D warnings` clean
|
||||
- **Tests:** 1243 Rust tests passing; Playwright suite in `solitaire_server/e2e/`
|
||||
- **HEAD:** `6193d31` (`fix(engine): centre modal cards within usable area (status-bar + gesture-bar)`)
|
||||
- **Latest tag:** `v0.39.0`
|
||||
- **Working tree:** documentation and analytics-test follow-up changes pending (`CHANGELOG.md`, `SESSION_HANDOFF.md`, `docs/card-game-integration.md`, `solitaire_data/src/matomo_client.rs`, `solitaire_engine/src/analytics_plugin.rs`)
|
||||
- **Latest verification in this follow-up:** `cargo test -p solitaire_core`; Matomo client/plugin targeted tests.
|
||||
- **Full previous gate:** Claude reported recent card_game work pushed to origin and `cargo test` / `clippy` gates passing before the changelog follow-up.
|
||||
|
||||
---
|
||||
|
||||
## What shipped since the last handoff (v0.35.1 → present, 2026-06-02)
|
||||
## What shipped since v0.39.0
|
||||
|
||||
| Commit | Summary |
|
||||
|--------|---------|
|
||||
| `64f975e` | 14 cross-platform UX/UI fixes from 500-game audit |
|
||||
| `763fdb4` | Fix input: hit-test deck at correct position; accept waste click |
|
||||
| `1cdb78c` | cargo fmt; add analytics domain to CSP |
|
||||
| `baf524e` | Rebuild Bevy canvas WASM; add SolitaireGame interactive API |
|
||||
| `9ff0585` | Remove Quaternions registry auth; canvas WASM drift guard |
|
||||
| `de7ae16` | Delay first-run modal until splash screen despawns |
|
||||
| `8b736ca` | Debug drag failures (temp logging, removed in next commit) |
|
||||
| `8b262af` | Clamp wgpu surface to CSS pixels on HiDPI (prevented WASM panic) |
|
||||
| `d45b7cb` | Add Playwright e2e test suite for web routes |
|
||||
| `2cf7282` | Add `window.__FERROUS_DEBUG__` bridge to `/play` for automation |
|
||||
|
||||
**Key audit bugs fixed (all 7 from 500-game UX audit):** timer-after-undo, radial-menu clamping, Android resume flash, tab-hidden timer, orphaned tmp files, drag threshold 4→6px, Draw-1 recycle doc comment.
|
||||
|
||||
**HiDPI wgpu fix:** `WindowResolution::default().with_scale_factor_override(1.0)` added to the Bevy canvas app. Root cause was physical pixels (CSS×DPR) exceeding WebGL2's 2048px per-dimension limit on HiDPI displays.
|
||||
|
||||
**E2E test architecture:** three-tier — Rust unit tests → Playwright smoke/review specs → cycle regression gate. Debug bridge contract in `docs/testing-architecture.md`.
|
||||
- Browser Bevy canvas route and `window.__FERROUS_DEBUG__` automation bridge landed, with Playwright coverage for `/play`.
|
||||
- In-place `card_game` / `klondike` rewrite phases are complete through the latest follow-up:
|
||||
- `5e87358` integrates upstream deps cleanly.
|
||||
- `ae1ecc8` unifies `Suit` / `Rank` with upstream `card_game` types.
|
||||
- `d864d98` routes klondike/card imports through `solitaire_core`.
|
||||
- `9bcf13d`, `56e3b62`, `26f1b00` finish schema-v3 migration coverage, undo/recycle score correctness, and rewrite-plan docs.
|
||||
- Android keystore wiring, Android build-script hardening, server auth/runtime hardening, and modal safe-area centering have landed.
|
||||
- `CHANGELOG.md` has been caught up from `v0.34.0` through current unreleased work; verify it before committing because it was started by a rate-limited Claude workflow and then manually tightened.
|
||||
- Matomo analytics was re-reviewed: `MatomoClient` and `AnalyticsPlugin` are wired through `CoreGamePlugin` on non-wasm targets, and targeted tests now cover opt-in client creation, event encoding, buffer trimming, and analytics mode labels.
|
||||
|
||||
---
|
||||
|
||||
## What shipped before v0.35.1
|
||||
## Historical notes before v0.39.0
|
||||
|
||||
See git log. CHANGELOG.md currently ends at v0.33.0 (documentation debt, low priority).
|
||||
See git log and `CHANGELOG.md`. The changelog now includes `v0.34.0` through `v0.39.0`, plus current unreleased work.
|
||||
|
||||
---
|
||||
|
||||
@@ -110,12 +101,7 @@ Three bugs fixed:
|
||||
|
||||
## Open punch list
|
||||
|
||||
### 1. CHANGELOG documentation debt
|
||||
|
||||
CHANGELOG.md currently ends at v0.33.0. All post-v0.33.0 work is in git log. Low
|
||||
priority — git log is authoritative.
|
||||
|
||||
### 2. Android APK launch verification (Option A)
|
||||
### 1. Android APK launch verification (Option A)
|
||||
|
||||
Physical device test: install the latest APK on a real Android device (not AVD),
|
||||
confirm:
|
||||
@@ -129,12 +115,18 @@ confirm:
|
||||
This has never been gated in CI. AVD `adb shell input tap` doesn't deliver real
|
||||
touch events, so physical-device smoke testing is the only gate.
|
||||
|
||||
### 3. Matomo analytics wiring
|
||||
### 2. Matomo analytics live validation
|
||||
|
||||
`Settings` has `analytics_enabled: bool` and `matomo_url: Option<String>` but no
|
||||
engine code consumes them — the analytics toggle in Settings is a no-op. If
|
||||
analytics are ever needed, the Matomo HTTP Tracking API client needs to be written
|
||||
and wired to `GameStateResource` events.
|
||||
`Settings` has `analytics_enabled`, `matomo_url`, and `matomo_site_id`; the engine
|
||||
consumes them via `AnalyticsPlugin` on non-wasm targets. Remaining work is live
|
||||
validation against the deployed Matomo instance:
|
||||
- Configure `matomo_url` and opt in through Settings.
|
||||
- Play a short session that starts a game, wins or forfeits, and unlocks or
|
||||
verifies an achievement event path if practical.
|
||||
- Confirm Matomo receives `Game / Start`, `Game / Won` or `Game / Forfeit`, and
|
||||
any achievement events.
|
||||
- Decide whether the web/WASM route should eventually use browser-side tracking,
|
||||
since the native `AnalyticsPlugin` is intentionally gated out on wasm32.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
**Context:** A collaborator ([Quaternions](https://git.aleshym.co/Quaternions/card_game)) is building a pure-logic Klondike library in Rust. This document maps what that library currently provides against what Ferrous Solitaire's `solitaire_core` crate requires.
|
||||
|
||||
**Approach:** Most gaps are closed in Ferrous Solitaire's own `solitaire_core` crate via a wrapper/adapter layer. Gaps 1, 3, and 4 have been addressed upstream. Integration is ready to begin.
|
||||
**Approach:** Integration is complete. Upstream `card_game` / `klondike` now owns
|
||||
authoritative Klondike rules, session history, undo snapshots, and solving.
|
||||
Ferrous keeps product-specific scoring, persistence, rendering DTOs, game modes,
|
||||
and typed UI errors in `solitaire_core`.
|
||||
|
||||
---
|
||||
|
||||
@@ -42,10 +45,12 @@
|
||||
|
||||
---
|
||||
|
||||
## What Ferrous Solitaire's `solitaire_core` Needs (Gaps)
|
||||
## What Ferrous Solitaire's `solitaire_core` Still Owns
|
||||
|
||||
### 1. Scoring — remaining adapter responsibilities
|
||||
Ferrous uses **Windows XP Standard** scoring. The exact table already implemented in `solitaire_core/src/scoring.rs`:
|
||||
Ferrous uses **Windows XP Standard** scoring. The upstream library handles the
|
||||
per-move counters and configurable deltas; Ferrous adds the product-specific
|
||||
parts in `GameState` / `KlondikeAdapter`.
|
||||
|
||||
| Event | Delta | Handled by |
|
||||
|---|---|---|
|
||||
@@ -61,11 +66,13 @@ Ferrous uses **Windows XP Standard** scoring. The exact table already implemente
|
||||
|
||||
Reference: <https://www.solitaireparadise.com/games_list/klondike_solitaire_scoring.html>
|
||||
|
||||
**Undo penalty:** `SessionState::score()` = `KlondikeStats.score(&scoring) + undos × undo_penalty`. The −15 undo penalty is built into `SessionConfig` (default). Once `GameState` fully delegates to `Session`, our `KlondikeAdapter::score_for_undo()` helper becomes redundant.
|
||||
**Undo penalty:** `SessionState::score()` = `KlondikeStats.score(&scoring) + undos × undo_penalty`. Ferrous still owns the exact user-visible score because it must restore the pre-move score when undoing recycle penalties and then apply the product's undo penalty.
|
||||
|
||||
**Recycle penalty note:** `ScoringConfig::recycle` is a flat delta (default 0 = always free). WXP allows a fixed number of free recycles before charging a penalty, which the upstream library cannot express with a single delta. Our adapter tracks `recycle_count` from `KlondikeStats` and applies the penalty only beyond the free allowance.
|
||||
|
||||
**In our wrapper:** Configure `ScoringConfig` with the WXP deltas for the five events upstream handles (including undo via `SessionConfig`). Implement recycle-with-free-allowance, score floor, and time bonus in the adapter.
|
||||
**In our wrapper:** `KlondikeAdapter::config_for` configures the upstream rules
|
||||
and scoring deltas. `GameState` applies recycle-with-free-allowance, score floor,
|
||||
time bonus, game-mode suppression, and undo score restoration.
|
||||
|
||||
### 2. Game Modes
|
||||
Ferrous has three modes that alter scoring and undo behaviour:
|
||||
@@ -78,7 +85,9 @@ Ferrous has three modes that alter scoring and undo behaviour:
|
||||
|
||||
Zen is intended for relaxed play where the score does not matter. Challenge is a timed daily puzzle where the no-undo constraint is the difficulty mechanic.
|
||||
|
||||
**In our wrapper:** Add `GameMode` to `solitaire_core::GameState`; intercept undo calls and scoring deltas in the adapter before delegating to `KlondikeState`.
|
||||
**In our wrapper:** `GameMode` lives on `solitaire_core::GameState`; undo and
|
||||
scoring behavior are applied before/after delegating legal moves to the upstream
|
||||
session.
|
||||
|
||||
### 3. Solvability Solver *(upstream merged — card_game v0.4.0)*
|
||||
`card_game v0.4.0` ships `Session::solve()` — a budget-bounded DFS that returns `Result<Option<Solution<G>>, SolveError>`. `SolveError` has two variants:
|
||||
@@ -87,9 +96,13 @@ Zen is intended for relaxed play where the score does not matter. Challenge is a
|
||||
|
||||
`Solution<G>` contains the winning move sequence as `Vec<StateSnapshot<G>>`; `clean_solution()` removes cycles. `Session::solve()` uses `SessionConfig::solve_moves_budget` and `SessionConfig::solve_states_budget` (defaults: 100 000 each).
|
||||
|
||||
Our 767-line `solitaire_core::solver` reimplements the full game rules to run the DFS; `session.solve()` replaces it entirely. The solver will be removed once the `Session<Klondike>` is wired into `GameState`.
|
||||
The old local DFS has been replaced. `solitaire_core::solver` is now a small
|
||||
adapter around `Session::solve()` that preserves the engine-facing
|
||||
`SolverResult`, `SolverConfig`, and first-move payload contract.
|
||||
|
||||
**In our wrapper:** Replace `solitaire_core::solver` with `session.solve()`. Map `Ok(Some(_))` → Winnable, `Ok(None)` → Unwinnable, `Err(_)` → Inconclusive.
|
||||
**In our wrapper:** `solve_game_state` calls `session.solve()` with the requested
|
||||
budgets. It maps `Ok(Some(_))` → Winnable, `Ok(None)` → Unwinnable, and budget
|
||||
errors → Inconclusive.
|
||||
|
||||
### 4. `take_from_foundation` House Rule *(upstream merged — v0.3.0)*
|
||||
`MoveFromFoundationConfig` is now part of `KlondikeConfig`. When set to `Disallowed`, `is_instruction_valid` blocks foundation → tableau instructions.
|
||||
@@ -135,7 +148,9 @@ Ferrous tracks `PileType::Waste` as a distinct pile. `klondike` folds waste into
|
||||
### 8. Undo Stack Approach *(resolved — not an issue)*
|
||||
`card_game v0.4.0` `Session` uses snapshot-based undo: `SessionState` stores `Vec<StateSnapshot<G>>` where each entry holds the pre-move game state and the instruction. Undo pops the last snapshot and restores state directly — O(1), matching our existing `GameState.undo_stack`.
|
||||
|
||||
**Resolution:** Use `Session`'s built-in snapshot history. Our `GameState.undo_stack: VecDeque<StateSnapshot>` will be removed once `GameState` is fully migrated to delegate to `Session`.
|
||||
**Resolution:** `GameState` uses `Session`'s built-in snapshot history. Ferrous
|
||||
keeps parallel score/recycle metadata so undo can restore product-specific score
|
||||
state that upstream snapshots do not own.
|
||||
|
||||
---
|
||||
|
||||
@@ -144,12 +159,12 @@ Ferrous tracks `PileType::Waste` as a distinct pile. `klondike` folds waste into
|
||||
Steps in dependency order. Upstream issues #10, #11, and the solver are all merged.
|
||||
|
||||
1. ✅ **Add `klondike = "0.3.0"` / `card_game = "0.4.0"` as dependencies** of `solitaire_core`; `KlondikeAdapter` wraps `KlondikeConfig` and exposes scoring helpers.
|
||||
2. **Map pile types** — project `klondike`'s stock face-up half as `PileType::Waste`; expose the same `HashMap<PileType, Pile>` the engine already reads. Wire `Session<Klondike>` into `KlondikeAdapter` (gap 7).
|
||||
3. ✅ **Configure `KlondikeConfig`** — set `move_from_foundation: MoveFromFoundationConfig::Disallowed` by default; wire the user's house-rule toggle to `Allowed` (gap 4, upstream).
|
||||
2. ✅ **Map pile types** — project `klondike`'s stock face-up half as the engine's waste pile and expose renderer-facing pile snapshots.
|
||||
3. ✅ **Configure `KlondikeConfig`** — set `move_from_foundation: MoveFromFoundationConfig::Allowed` by default; wire the user's settings toggle to `Disallowed` when foundation returns are disabled (gap 4, upstream).
|
||||
4. ✅ **Port scoring** — pass WXP deltas into `ScoringConfig`; `SessionConfig::undo_penalty` handles undo; implement recycle-with-free-allowance, score floor, and time bonus in the adapter (gap 1).
|
||||
5. ✅ **Port `GameMode`** — intercept undo + scoring in the adapter based on mode (gap 2).
|
||||
6. **Replace solver** — call `session.solve()` with budgets from our `SolverConfig`; map `Ok(Some)` → Winnable, `Ok(None)` → Unwinnable, `Err` → Inconclusive (gap 3, upstream).
|
||||
7. **Implement `serde`** — define `SavedInstruction` + `SavedStateSnapshot` newtypes; serialise session history; migrate save-file schema (gap 5).
|
||||
6. ✅ **Replace solver** — call `session.solve()` with budgets from `SolverConfig`; map `Ok(Some)` → Winnable, `Ok(None)` → Unwinnable, `Err` → Inconclusive (gap 3, upstream).
|
||||
7. ✅ **Implement `serde`** — serialise schema v4 with upstream `KlondikeInstruction`; auto-migrate schema v3 via `SavedInstruction` compatibility types.
|
||||
|
||||
---
|
||||
|
||||
@@ -192,5 +207,5 @@ The script enforces:
|
||||
- Upstream scoring + config PRs: #12 (closes #11), #13 (closes #10)
|
||||
- Upstream solver PR: #14
|
||||
- `solitaire_core` source: `solitaire_core/src/`
|
||||
- Scoring spec: `solitaire_core/src/scoring.rs`
|
||||
- Scoring implementation: `solitaire_core/src/game_state.rs`, `solitaire_core/src/klondike_adapter.rs`
|
||||
- Architecture overview: `ARCHITECTURE.md`
|
||||
|
||||
@@ -114,3 +114,62 @@ fn url_encode(s: &str) -> String {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pending(client: &MatomoClient) -> Vec<String> {
|
||||
client.pending.lock().expect("pending lock").clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_buffers_encoded_matomo_query() {
|
||||
let client = MatomoClient::new(
|
||||
"https://analytics.example.com/",
|
||||
7,
|
||||
Some("alice bob".into()),
|
||||
);
|
||||
|
||||
client.event("Game Flow", "Won+Fast", Some("draw three"), Some(42.5));
|
||||
|
||||
let pending = pending(&client);
|
||||
assert_eq!(pending.len(), 1);
|
||||
let query = &pending[0];
|
||||
assert!(query.contains("idsite=7"));
|
||||
assert!(query.contains("rec=1"));
|
||||
assert!(query.contains("e_c=Game%20Flow"));
|
||||
assert!(query.contains("e_a=Won%2BFast"));
|
||||
assert!(query.contains("e_n=draw%20three"));
|
||||
assert!(query.contains("e_v=42.5"));
|
||||
assert!(query.contains("uid=alice%20bob"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_buffer_drops_oldest_entries_when_capacity_exceeded() {
|
||||
let client = MatomoClient::new("https://analytics.example.com", 1, None);
|
||||
|
||||
for idx in 0..101 {
|
||||
client.event("Game", "Start", Some(&format!("event-{idx}")), None);
|
||||
}
|
||||
|
||||
let pending = pending(&client);
|
||||
assert_eq!(pending.len(), 51);
|
||||
assert!(
|
||||
pending[0].contains("event-50"),
|
||||
"oldest retained event should be event-50, got {}",
|
||||
pending[0]
|
||||
);
|
||||
assert!(
|
||||
pending[50].contains("event-100"),
|
||||
"newest retained event should be event-100, got {}",
|
||||
pending[50]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_encode_leaves_unreserved_bytes_and_escapes_everything_else() {
|
||||
assert_eq!(url_encode("AZaz09-_.~"), "AZaz09-_.~");
|
||||
assert_eq!(url_encode("a b+c/d?"), "a%20b%2Bc%2Fd%3F");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,3 +204,61 @@ fn mode_str(mode: GameMode) -> &'static str {
|
||||
GameMode::Difficulty(_) => "difficulty",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use solitaire_core::game_state::DifficultyLevel;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn client_for_requires_analytics_opt_in() {
|
||||
let settings = Settings {
|
||||
analytics_enabled: false,
|
||||
matomo_url: Some("https://analytics.example.com".into()),
|
||||
..Settings::default()
|
||||
};
|
||||
|
||||
assert!(client_for(&settings).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_for_requires_matomo_url() {
|
||||
let settings = Settings {
|
||||
analytics_enabled: true,
|
||||
matomo_url: None,
|
||||
..Settings::default()
|
||||
};
|
||||
|
||||
assert!(client_for(&settings).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_for_creates_client_when_enabled_and_configured() {
|
||||
let settings = Settings {
|
||||
analytics_enabled: true,
|
||||
matomo_url: Some("https://analytics.example.com".into()),
|
||||
matomo_site_id: 2,
|
||||
sync_backend: SyncBackend::SolitaireServer {
|
||||
url: "https://solitaire.example.com".into(),
|
||||
username: "alice".into(),
|
||||
avatar_url: None,
|
||||
},
|
||||
..Settings::default()
|
||||
};
|
||||
|
||||
assert!(client_for(&settings).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_labels_match_analytics_payload_contract() {
|
||||
assert_eq!(mode_str(GameMode::Classic), "classic");
|
||||
assert_eq!(mode_str(GameMode::Zen), "zen");
|
||||
assert_eq!(mode_str(GameMode::Challenge), "challenge");
|
||||
assert_eq!(mode_str(GameMode::TimeAttack), "time_attack");
|
||||
assert_eq!(
|
||||
mode_str(GameMode::Difficulty(DifficultyLevel::Grandmaster)),
|
||||
"difficulty"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user