docs: card_game integration gap analysis #76
Reference in New Issue
Block a user
Delete Branch "docs/card-game-integration"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds
docs/card-game-integration.md— a full comparison of what @Quaternions'scard_gamelibrary already provides versus whatsolitaire_coreneeds, plus a prioritised list of work items.What the doc covers
card_game+klondikehave today (Card primitives, Pile, Game trait, Session/undo, Draw-1/3, auto-complete, benchmarks)take_from_foundationhouse ruleMoveErrorreturns (currentlybool)No code changes — documentation only.
If you can find an online article that has the exact scoring rules I can work on improving the scoring, that should be pretty simple. Time bonus will need to be integrated externally due to dependence on non-portable primitives (std::time not portable to wasm).
Quaternions/card_game#10 ✅
What does this entail? I don't know the difference and couldn't tell the difference when playing.
I assume this means a configuration option to disable taking from foundation. Can do.
Quaternions/card_game#11 ✅
This can be implemented externally from card_game (i.e. in solitaire_core) with newtypes
I don't see the point of this. Invalid moves are not expressible with KlondikeInstruction other than referring to a SkipCard incorrectly, which is not possible from the user's POV since the KlondikeInstruction is generated by the game code using game entity layout. Therefore, solitaire_core will be the one to know when a move is invalid because it fails to construct the move, and can give an error accordingly if so desired.
This is not an issue. Yes it is O(n), but the performance is 1000000 moves / second which means a maximum game length of ~192 moves will always complete in 0.02ms and feel instantaneous.
Thanks for the detailed response!
Scoring sub-rules
The rules we use are the Windows XP Standard scoring variant. Here is the exact table already implemented in
solitaire_core/src/scoring.rs:score.max(0)always700_000 / elapsed_secondsA commonly cited reference: https://www.solitaireparadise.com/games_list/klondike_solitaire_scoring.html
Agreed on the time bonus — that stays in
solitaire_coresince it depends on wall-clock time which is not wasm-portable.Classic / Challenge / Zen
The three modes share the same Klondike rules but differ on scoring and undo:
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.
take_from_foundationTo clarify: this is about enabling an optional move that is off by default. When the flag is on, the player is allowed to move the top card of a foundation pile back down to a compatible tableau column. The standard rule is that foundation cards cannot be moved back; this is the house-rule relaxation that makes stuck games more recoverable. So it is the opposite of disabling — it is an extra legal instruction guarded by a config flag.
JSON serialisation
Agreed — we will handle it in
solitaire_corevia newtypes. No changes needed on your side.Typed
MoveErrorYour reasoning makes sense. Because
KlondikeInstructionis always constructed by game code from valid entity layout, the only real failure mode is thatsolitaire_corefails to construct the instruction in the first place — so the error lives at our boundary, not yours. We will map it there.Undo performance
1 000 000 moves/sec and a max game length of ~192 moves = 0.02 ms worst case. Agreed — not worth changing. We will accept the replay approach.
The scoring specifics and challenge modes are completely configurable from within solitaire_core using the latest version of card_game + klondike. Looks like this is ready to start implementing.
Published a new crate version with a solver. The history is also changed to use snapshots instead of replay.
@@ -0,0 +51,4 @@| Waste → tableau | +5 | `KlondikeStats` / `ScoringConfig::move_to_tableau` ✅ || Flip face-down tableau card | +5 | `KlondikeStats` / `ScoringConfig::flip_up_bonus` ✅ || Foundation → tableau | −15 | `KlondikeStats` / `ScoringConfig::move_from_foundation` ✅ || Undo | −15 | **Our adapter** (klondike has no undo event) |Undo is indeed not present in klondike, but it is implemented by card_game::Session, undo score is tracked in card_game::SessionStats.
@@ -0,0 +77,4 @@**In our wrapper:** Add `GameMode` to `solitaire_core::GameState`; intercept undo calls and scoring deltas in the adapter before delegating to `KlondikeState`.### 3. Solvability SolverThe engine has a Settings toggle — "Winnable deals only" — backed by a DFS solver with canonical-state memoisation (`solitaire_core::solver`). The solver classifies each deal as `Winnable`, `Unwinnable`, or `Inconclusive` (budget exceeded). `klondike` has no equivalent.Added a solver to card_game 0.4.0.
@@ -0,0 +115,4 @@**In our wrapper:** Project the face-up half of `klondike`'s stock `Pile` as `PileType::Waste` when building pile snapshots for the engine.### 8. Undo Stack Approach *(resolved — not an issue)*`Session` undoes by replaying all moves from the seed — O(n). Benchmark shows 1 000 000 moves/second throughput; a maximum game length of ~192 moves gives a worst-case undo time of 0.02 ms — imperceptible on any hardware.Undo has been changed to a stack-based approach in card_game 0.4.0.
Thanks for the correction and for the new release!
Undo scoring
You are right — undo penalty is included via
SessionState::score()=game_score + undos × undo_penalty. I had misread the v0.2.0 source and assumed the penalty was absent. Will fix the doc.Solver
Looking at the new
Session::solve()— much cleaner than what we wrote. We will replace our 767-line solver withsession.solve()once the session is wired up. Can you clarify whatSolveError::MovesBudgetExceededvsSolveError::StatesBudgetExceededmaps to in terms of ourSolverResult::Inconclusive? I assume both cases correspond toInconclusiveon our side.Snapshot-based history
Also noticed
SessionStatenow uses snapshot-based undo (stores pre-moveGsnapshot + instruction) instead of replaying from seed. This closes Gap 8 from the original doc — undo is O(1) on the session side too.@@ -0,0 +138,4 @@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).HashMap<PileType, Pile>is not going to work because the piles are owned by KlondikeState. There should be no HashMap required to display the pile contents since each pile / stack is rendered explicitly in a fixed location. Attempted moves can be constructed directly as a KlondikeInstruction with no need for (PileType, PileType, usize) or HashMap.Pull request closed