Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aebb401c44 | |||
| a550a0cdf9 | |||
| 8f5193035b | |||
| c21c0ebf99 | |||
| ccccdd2b40 |
@@ -1,2 +0,0 @@
|
|||||||
[registries.Quaternions]
|
|
||||||
index = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/"
|
|
||||||
Generated
+12
-65
@@ -364,12 +364,6 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayvec"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/"
|
|
||||||
checksum = "813440870d646c57c222c1d713dc4e3ddcb2919c3801564d767d85d7bf2afee4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "as-raw-xcb-connection"
|
name = "as-raw-xcb-connection"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -907,7 +901,7 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c9cf7a3ee41342dd7b5a5d82e200d0e8efb933169247fce853b4ad633d51e87d"
|
checksum = "c9cf7a3ee41342dd7b5a5d82e200d0e8efb933169247fce853b4ad633d51e87d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bevy_ecs_macros",
|
"bevy_ecs_macros",
|
||||||
"bevy_platform",
|
"bevy_platform",
|
||||||
"bevy_ptr",
|
"bevy_ptr",
|
||||||
@@ -1144,7 +1138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e931fa969f89c83498b22c97432383afe90e90fd1a5e04fa07be8da4d3bcac84"
|
checksum = "e931fa969f89c83498b22c97432383afe90e90fd1a5e04fa07be8da4d3bcac84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bevy_reflect",
|
"bevy_reflect",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"glam 0.30.10",
|
"glam 0.30.10",
|
||||||
@@ -1709,7 +1703,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
|
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"constant_time_eq",
|
"constant_time_eq",
|
||||||
@@ -1885,15 +1879,6 @@ dependencies = [
|
|||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "card_game"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/"
|
|
||||||
checksum = "38b68e4fb32f8a1f92edf8488c012f6d8af71491a2f9f8a855362d7eaf1a2d0c"
|
|
||||||
dependencies = [
|
|
||||||
"arrayvec 0.7.6 (sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/)",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cbc"
|
name = "cbc"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1954,17 +1939,6 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487"
|
checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures 0.3.0",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
@@ -4352,23 +4326,13 @@ dependencies = [
|
|||||||
"triple_buffer",
|
"triple_buffer",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "klondike"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/"
|
|
||||||
checksum = "0bce541f9b14e9d9d8c9b17d5df40bd0a017709b61d9be8ad5bab7b19a1a0152"
|
|
||||||
dependencies = [
|
|
||||||
"card_game",
|
|
||||||
"rand 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kurbo"
|
name = "kurbo"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb"
|
checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"euclid",
|
"euclid",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
@@ -4776,7 +4740,7 @@ version = "27.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
|
checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -5983,16 +5947,6 @@ dependencies = [
|
|||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
|
||||||
dependencies = [
|
|
||||||
"chacha20",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -6031,12 +5985,6 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_distr"
|
name = "rand_distr"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -7032,7 +6980,6 @@ dependencies = [
|
|||||||
name = "solitaire_core"
|
name = "solitaire_core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"klondike",
|
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -7555,7 +7502,7 @@ version = "0.5.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
|
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@@ -7654,7 +7601,7 @@ version = "0.9.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b"
|
checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"grid",
|
"grid",
|
||||||
"serde",
|
"serde",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
@@ -7923,7 +7870,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"log",
|
"log",
|
||||||
@@ -7937,7 +7884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"log",
|
"log",
|
||||||
@@ -9097,7 +9044,7 @@ version = "27.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
|
checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
@@ -9121,7 +9068,7 @@ version = "27.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
|
checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
@@ -9171,7 +9118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
|
checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_system_properties",
|
"android_system_properties",
|
||||||
"arrayvec 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"arrayvec",
|
||||||
"ash",
|
"ash",
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ solitaire_core = { path = "solitaire_core" }
|
|||||||
solitaire_sync = { path = "solitaire_sync" }
|
solitaire_sync = { path = "solitaire_sync" }
|
||||||
solitaire_data = { path = "solitaire_data" }
|
solitaire_data = { path = "solitaire_data" }
|
||||||
solitaire_engine = { path = "solitaire_engine" }
|
solitaire_engine = { path = "solitaire_engine" }
|
||||||
klondike = { version = "0.2.0", registry = "Quaternions" }
|
|
||||||
|
|
||||||
# Bevy with `default-features = false` to avoid the unused
|
# Bevy with `default-features = false` to avoid the unused
|
||||||
# `bevy_audio → rodio + symphonia + cpal 0.15 + alsa 0.9` chain.
|
# `bevy_audio → rodio + symphonia + cpal 0.15 + alsa 0.9` chain.
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Integrating `card_game` / `klondike` as the Solitaire Core
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What `card_game` + `klondike` Already Has
|
||||||
|
|
||||||
|
### `card_game` crate (generic primitives) — v0.4.0
|
||||||
|
| Feature | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `Card` (Deck + Suit + Rank packed in 1 byte) | `NonZeroU8` layout — no heap allocation |
|
||||||
|
| `Suit`, `Rank`, `Deck` enums | Full A→K, 4 suits, up to 4 deck IDs |
|
||||||
|
| `Stack<CAP>` | Const-generic `ArrayVec` wrapper |
|
||||||
|
| `Pile<DN, UP>` | Face-down + face-up stacks; `flip_up`, `pop_flip_up` |
|
||||||
|
| `Game` trait | `possible_instructions`, `is_instruction_valid`, `process_instruction`, `is_win` |
|
||||||
|
| `Session` | Wraps a `Game`; snapshot-based undo (O(1)), score including undo penalty |
|
||||||
|
| `Session::solve()` | Built-in DFS solver with move/state budgets; returns `Solution<G>` or `SolveError` |
|
||||||
|
| `StateSnapshot<G>` | Pre-move state + instruction; used by snapshot history and `Solution` |
|
||||||
|
| `SessionState::score()` | = `game_score + undos × undo_penalty` (−15 by default via `SessionConfig`) |
|
||||||
|
| `SessionConfig` | `undo_penalty`, `solve_moves_budget`, `solve_states_budget` |
|
||||||
|
|
||||||
|
### `klondike` crate (Klondike rules) — v0.3.0
|
||||||
|
| Feature | Notes |
|
||||||
|
|---|---|
|
||||||
|
| 7 tableau + 4 foundation + 1 stock | Fully dealt from a seeded RNG |
|
||||||
|
| Draw-1 / Draw-3 config | `KlondikeConfig::draw_stock` (`DrawStockConfig`) |
|
||||||
|
| `MoveFromFoundationConfig` | `Allowed` (upstream default) / `Disallowed`; controls foundation → tableau rule |
|
||||||
|
| `ScoringConfig` | Configurable deltas: `move_to_foundation` (+10), `flip_up_bonus` (+5), `move_to_tableau` (+5), `move_from_foundation` (−15), `recycle` (0 by default) |
|
||||||
|
| `KlondikeStats::score(&config)` | Computes score from per-event counters × `ScoringConfig` deltas |
|
||||||
|
| `KlondikeStats` counters | `move_to_foundation_count`, `flip_up_bonus_count`, `move_to_tableau_count`, `move_from_foundation_count`, `recycle_count`, `moves` |
|
||||||
|
| Foundation placement (Ace start, suit-matched A→K) | ✅ |
|
||||||
|
| Tableau placement (alternating colour, K on empty) | ✅ |
|
||||||
|
| Multi-card stack moves (via `SkipCards`) | ✅ |
|
||||||
|
| `RotateStock` (recycle waste → stock) | ✅ |
|
||||||
|
| `is_win_trivial` (all face-down cards cleared) | Auto-complete trigger |
|
||||||
|
| `get_auto_move` / `get_sorted_moves` | Priority-ranked move suggestion (take `&KlondikeConfig`) |
|
||||||
|
| Benchmark suite (`klondike-bench`) | 1 000-game throughput test |
|
||||||
|
| CLI display (`klondike-cli`) | Terminal renderer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Ferrous Solitaire's `solitaire_core` Needs (Gaps)
|
||||||
|
|
||||||
|
### 1. Scoring — remaining adapter responsibilities
|
||||||
|
Ferrous uses **Windows XP Standard** scoring. The exact table already implemented in `solitaire_core/src/scoring.rs`:
|
||||||
|
|
||||||
|
| Event | Delta | Handled by |
|
||||||
|
|---|---|---|
|
||||||
|
| Any card → foundation | +10 | `KlondikeStats` / `ScoringConfig::move_to_foundation` ✅ |
|
||||||
|
| 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 | `SessionStats` / `SessionConfig::undo_penalty` ✅ |
|
||||||
|
| Recycle (Draw-1, after 1st free) | −100 | **Our adapter** — see below |
|
||||||
|
| Recycle (Draw-3, after 3rd free) | −20 | **Our adapter** — see below |
|
||||||
|
| Score floor | `score.max(0)` always | **Our adapter** |
|
||||||
|
| Time bonus on win | `700_000 / elapsed_seconds` | **Our adapter** (not wasm-portable) |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### 2. Game Modes
|
||||||
|
Ferrous has three modes that alter scoring and undo behaviour:
|
||||||
|
|
||||||
|
| Mode | Scoring | Undo |
|
||||||
|
|---|---|---|
|
||||||
|
| **Classic** | Full WXP scoring (table above) | Allowed (−15 penalty) |
|
||||||
|
| **Zen** | All deltas suppressed — score stays 0 | Allowed (no penalty) |
|
||||||
|
| **Challenge** | Full WXP scoring | **Disabled** — returns an error |
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
- `MovesBudgetExceeded` — equivalent to our `SolverResult::Inconclusive`
|
||||||
|
- `StatesBudgetExceeded` — equivalent to our `SolverResult::Inconclusive`
|
||||||
|
|
||||||
|
`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`.
|
||||||
|
|
||||||
|
**In our wrapper:** Replace `solitaire_core::solver` with `session.solve()`. Map `Ok(Some(_))` → Winnable, `Ok(None)` → Unwinnable, `Err(_)` → 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.
|
||||||
|
|
||||||
|
**Important:** The upstream default is `MoveFromFoundationConfig::Allowed`. Ferrous Solitaire uses the standard rule (foundation cards cannot be moved back) as the default, with the house rule as an opt-in. Our adapter explicitly sets `Disallowed` in the default `KlondikeConfig` and switches to `Allowed` only when the user toggles the house-rule option.
|
||||||
|
|
||||||
|
**In our wrapper:** Construct `KlondikeConfig { move_from_foundation: MoveFromFoundationConfig::Disallowed, .. }` by default; mirror the user's settings toggle to `Allowed`. No custom intercept needed — `klondike` enforces the rule automatically.
|
||||||
|
|
||||||
|
### 5. JSON Serialisation / Persistence
|
||||||
|
`solitaire_core::GameState` serialises the full mid-game state to JSON via `serde` so the engine can save on exit and restore on launch. `KlondikeState` derives `Clone` + `Eq` + `Hash` but not `Serialize` / `Deserialize`. No upstream changes are needed — this is handled externally.
|
||||||
|
|
||||||
|
**Session history:** `StateSnapshot<G>` stores the pre-move game state and instruction. On load, the session is reconstructed from the serialised snapshot history — no full replay from seed needed.
|
||||||
|
|
||||||
|
**In our wrapper:** Serialise the `solitaire_core` wrapper struct using newtypes. Define `SavedInstruction` (a `Serialize + Deserialize` mirror of `KlondikeInstruction`) and `SavedStateSnapshot`. Reconstruct `SessionState` from the deserialised history. Schema version field lives on our wrapper.
|
||||||
|
|
||||||
|
### 6. Typed Move Errors
|
||||||
|
`solitaire_core::error::MoveError` returns structured errors the engine uses to trigger UI feedback (wrong-destination toast, stock-empty chime, etc.):
|
||||||
|
|
||||||
|
```
|
||||||
|
GameAlreadyWon
|
||||||
|
UndoStackEmpty
|
||||||
|
StockEmpty
|
||||||
|
InvalidSource
|
||||||
|
InvalidDestination
|
||||||
|
RuleViolation(String)
|
||||||
|
```
|
||||||
|
|
||||||
|
`KlondikeInstruction` is always constructed by game code from valid entity layout, so invalid moves are only detectable at `solitaire_core`'s construction boundary — the error lives there, not inside `klondike`.
|
||||||
|
|
||||||
|
**In our wrapper:** `MoveError` variants are generated when `solitaire_core` fails to construct a `KlondikeInstruction` from the player's requested move. No translation of `is_instruction_valid`'s bool return is required; by the time an instruction reaches `klondike`, it is already known to be structurally valid.
|
||||||
|
|
||||||
|
### 7. Waste Pile as Separate Concept
|
||||||
|
Ferrous tracks `PileType::Waste` as a distinct pile. `klondike` folds waste into `Stock` (the face-up half of the stock `Pile`). The engine's UI and scoring logic reference the waste pile directly; the mapping needs to be explicit.
|
||||||
|
|
||||||
|
**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)*
|
||||||
|
`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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Path (All work in `solitaire_core`)
|
||||||
|
|
||||||
|
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).
|
||||||
|
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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Does NOT Need to Change
|
||||||
|
|
||||||
|
- The `solitaire_engine` Bevy layer — it works against `solitaire_core` types; changes are isolated to `solitaire_core`.
|
||||||
|
- The `solitaire_sync` merge logic — operates on a `SyncPayload` DTO, independent of core card types.
|
||||||
|
- The `solitaire_server` — speaks only `SyncPayload` JSON, unaffected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Quaternions' repo: <https://git.aleshym.co/Quaternions/card_game>
|
||||||
|
- `card_game v0.4.0` release commit: `fa098f0d`
|
||||||
|
- `klondike v0.3.0` release commit: `f4c4e350`
|
||||||
|
- 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`
|
||||||
|
- Architecture overview: `ARCHITECTURE.md`
|
||||||
@@ -172,16 +172,13 @@ fn build_app_with_settings(
|
|||||||
// a 1-second ceiling when the app is backgrounded cuts wake-up frequency
|
// a 1-second ceiling when the app is backgrounded cuts wake-up frequency
|
||||||
// from ~60 Hz to ≤1 Hz, dramatically reducing background battery drain.
|
// from ~60 Hz to ≤1 Hz, dramatically reducing background battery drain.
|
||||||
//
|
//
|
||||||
// focused_mode uses reactive_low_power(100 ms) so the CPU only wakes when
|
// The focused mode stays Continuous so that card-slide animations remain
|
||||||
// an event arrives (touch, resize, etc.) or an animation system writes
|
// smooth. PresentMode::AutoVsync (set above) keeps the GPU capped at the
|
||||||
// RequestRedraw. The 100 ms ceiling is a fallback that ensures the game
|
// display refresh rate (~60 Hz) when foregrounded, which already prevents
|
||||||
// timer ticks at least 10×/s even with no input, while keeping the GPU
|
// the GPU from spinning at 200+ fps between vsync intervals.
|
||||||
// completely idle between frames when the board is static.
|
|
||||||
// PresentMode::AutoVsync (set above) still caps the GPU at the display
|
|
||||||
// refresh rate when frames do render.
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
app.insert_resource(WinitSettings {
|
app.insert_resource(WinitSettings {
|
||||||
focused_mode: UpdateMode::reactive_low_power(std::time::Duration::from_millis(100)),
|
focused_mode: UpdateMode::Continuous,
|
||||||
unfocused_mode: UpdateMode::reactive_low_power(std::time::Duration::from_secs(1)),
|
unfocused_mode: UpdateMode::reactive_low_power(std::time::Duration::from_secs(1)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ edition.workspace = true
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
klondike = { workspace = true }
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ use crate::deck::{Deck, deal_klondike};
|
|||||||
use crate::error::MoveError;
|
use crate::error::MoveError;
|
||||||
use crate::pile::{Pile, PileType};
|
use crate::pile::{Pile, PileType};
|
||||||
use crate::rules::{can_place_on_foundation, can_place_on_tableau, is_valid_tableau_sequence};
|
use crate::rules::{can_place_on_foundation, can_place_on_tableau, is_valid_tableau_sequence};
|
||||||
use crate::klondike_adapter::KlondikeAdapter;
|
use crate::scoring::{
|
||||||
use crate::scoring::compute_time_bonus as scoring_time_bonus;
|
compute_time_bonus as scoring_time_bonus, score_flip, score_move, score_recycle,
|
||||||
|
score_undo as scoring_undo,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
@@ -164,8 +166,6 @@ pub struct GameState {
|
|||||||
#[serde(default = "schema_v1")]
|
#[serde(default = "schema_v1")]
|
||||||
pub schema_version: u32,
|
pub schema_version: u32,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub adapter: KlondikeAdapter,
|
|
||||||
#[serde(skip)]
|
|
||||||
undo_stack: VecDeque<StateSnapshot>,
|
undo_stack: VecDeque<StateSnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +208,6 @@ impl GameState {
|
|||||||
recycle_count: 0,
|
recycle_count: 0,
|
||||||
take_from_foundation: true,
|
take_from_foundation: true,
|
||||||
schema_version: GAME_STATE_SCHEMA_VERSION,
|
schema_version: GAME_STATE_SCHEMA_VERSION,
|
||||||
adapter: KlondikeAdapter::new(draw_mode, true),
|
|
||||||
undo_stack: VecDeque::new(),
|
undo_stack: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +276,7 @@ impl GameState {
|
|||||||
self.recycle_count = self.recycle_count.saturating_add(1);
|
self.recycle_count = self.recycle_count.saturating_add(1);
|
||||||
if self.mode != GameMode::Zen {
|
if self.mode != GameMode::Zen {
|
||||||
let penalty =
|
let penalty =
|
||||||
KlondikeAdapter::score_for_recycle(self.recycle_count, self.draw_mode == DrawMode::DrawThree);
|
score_recycle(self.recycle_count, self.draw_mode == DrawMode::DrawThree);
|
||||||
self.score = (self.score + penalty).max(0);
|
self.score = (self.score + penalty).max(0);
|
||||||
}
|
}
|
||||||
self.move_count = self.move_count.saturating_add(1);
|
self.move_count = self.move_count.saturating_add(1);
|
||||||
@@ -414,7 +413,7 @@ impl GameState {
|
|||||||
let score_delta = if self.mode == GameMode::Zen {
|
let score_delta = if self.mode == GameMode::Zen {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
self.adapter.score_for_move(&from, &to)
|
score_move(&from, &to)
|
||||||
};
|
};
|
||||||
self.push_snapshot();
|
self.push_snapshot();
|
||||||
|
|
||||||
@@ -447,7 +446,7 @@ impl GameState {
|
|||||||
.append(&mut moved);
|
.append(&mut moved);
|
||||||
|
|
||||||
let flip_bonus = if flipped && self.mode != GameMode::Zen {
|
let flip_bonus = if flipped && self.mode != GameMode::Zen {
|
||||||
self.adapter.score_for_flip()
|
score_flip()
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
@@ -481,7 +480,7 @@ impl GameState {
|
|||||||
self.score = if self.mode == GameMode::Zen {
|
self.score = if self.mode == GameMode::Zen {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
(snapshot.score + KlondikeAdapter::score_for_undo()).max(0)
|
(snapshot.score + scoring_undo()).max(0)
|
||||||
};
|
};
|
||||||
self.move_count = snapshot.move_count;
|
self.move_count = snapshot.move_count;
|
||||||
self.is_won = false;
|
self.is_won = false;
|
||||||
@@ -727,7 +726,6 @@ impl GameState {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::card::{Card, Rank, Suit};
|
use crate::card::{Card, Rank, Suit};
|
||||||
use crate::klondike_adapter::KlondikeAdapter;
|
|
||||||
|
|
||||||
fn new_game() -> GameState {
|
fn new_game() -> GameState {
|
||||||
GameState::new(42, DrawMode::DrawOne)
|
GameState::new(42, DrawMode::DrawOne)
|
||||||
@@ -1128,7 +1126,7 @@ mod tests {
|
|||||||
let score_before = g.score;
|
let score_before = g.score;
|
||||||
g.draw().unwrap();
|
g.draw().unwrap();
|
||||||
g.undo().unwrap();
|
g.undo().unwrap();
|
||||||
let expected = (score_before + KlondikeAdapter::score_for_undo()).max(0);
|
let expected = (score_before + scoring_undo()).max(0);
|
||||||
assert_eq!(g.score, expected);
|
assert_eq!(g.score, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
//! Adapter bridging `solitaire_core` types to the upstream `klondike` crate.
|
|
||||||
//!
|
|
||||||
//! # Current scope (integration steps 1–4)
|
|
||||||
//!
|
|
||||||
//! [`KlondikeAdapter`] owns the authoritative [`KlondikeConfig`] and exposes
|
|
||||||
//! scoring helpers backed by [`ScoringConfig::DEFAULT`] (Windows XP Standard
|
|
||||||
//! values). [`GameState`] delegates scoring here so that klondike remains the
|
|
||||||
//! single source of truth for scoring constants.
|
|
||||||
//!
|
|
||||||
//! # Not yet implemented
|
|
||||||
//!
|
|
||||||
//! - Live [`klondike::Klondike`] shadow state (requires pile-mapping, step 2).
|
|
||||||
//! - Move validation via klondike's rule engine (step 2).
|
|
||||||
//! - DFS solver via [`klondike::KlondikeState`] (step 6).
|
|
||||||
|
|
||||||
use klondike::{DrawStockConfig, KlondikeConfig, MoveFromFoundationConfig, ScoringConfig};
|
|
||||||
|
|
||||||
use crate::game_state::DrawMode;
|
|
||||||
use crate::pile::PileType;
|
|
||||||
|
|
||||||
/// Bridges `solitaire_core` game config and scoring to the upstream `klondike` crate.
|
|
||||||
///
|
|
||||||
/// Holds a [`KlondikeConfig`] reflecting the current game settings and exposes
|
|
||||||
/// scoring helpers that read from [`ScoringConfig::DEFAULT`] (WXP values).
|
|
||||||
/// [`GameState`] uses this instead of calling `scoring.rs` functions directly.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct KlondikeAdapter {
|
|
||||||
config: KlondikeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for KlondikeAdapter {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.config.draw_stock == other.config.draw_stock
|
|
||||||
&& self.config.move_from_foundation == other.config.move_from_foundation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Eq for KlondikeAdapter {}
|
|
||||||
|
|
||||||
impl Default for KlondikeAdapter {
|
|
||||||
/// Returns an adapter with Draw-1 and `take_from_foundation = true`,
|
|
||||||
/// matching `GameState`'s own defaults. Used by `#[serde(skip)]`
|
|
||||||
/// field initialisation on deserialisation.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new(DrawMode::DrawOne, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KlondikeAdapter {
|
|
||||||
/// Create an adapter from the game's draw mode and foundation house-rule setting.
|
|
||||||
///
|
|
||||||
/// `take_from_foundation = true` maps to [`MoveFromFoundationConfig::Allowed`];
|
|
||||||
/// `false` maps to [`MoveFromFoundationConfig::Disallowed`].
|
|
||||||
pub fn new(draw_mode: DrawMode, take_from_foundation: bool) -> Self {
|
|
||||||
let config = KlondikeConfig {
|
|
||||||
draw_stock: match draw_mode {
|
|
||||||
DrawMode::DrawOne => DrawStockConfig::DrawOne,
|
|
||||||
DrawMode::DrawThree => DrawStockConfig::DrawThree,
|
|
||||||
},
|
|
||||||
move_from_foundation: if take_from_foundation {
|
|
||||||
MoveFromFoundationConfig::Allowed
|
|
||||||
} else {
|
|
||||||
MoveFromFoundationConfig::Disallowed
|
|
||||||
},
|
|
||||||
scoring: ScoringConfig::DEFAULT,
|
|
||||||
};
|
|
||||||
Self { config }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a reference to the underlying [`KlondikeConfig`].
|
|
||||||
///
|
|
||||||
/// Used by the solver and pile-mapping code added in later integration steps.
|
|
||||||
pub fn klondike_config(&self) -> &KlondikeConfig {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the foundation house-rule flag, keeping [`KlondikeConfig`] in sync.
|
|
||||||
pub fn set_take_from_foundation(&mut self, allowed: bool) {
|
|
||||||
self.config.move_from_foundation = if allowed {
|
|
||||||
MoveFromFoundationConfig::Allowed
|
|
||||||
} else {
|
|
||||||
MoveFromFoundationConfig::Disallowed
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Scoring helpers ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Score delta for a card move.
|
|
||||||
///
|
|
||||||
/// Reads from [`ScoringConfig`] (WXP Standard values):
|
|
||||||
/// - Any pile → Foundation: +10
|
|
||||||
/// - Waste → Tableau: +5
|
|
||||||
/// - Foundation → Tableau: −15
|
|
||||||
/// - All other moves: 0
|
|
||||||
pub fn score_for_move(&self, from: &PileType, to: &PileType) -> i32 {
|
|
||||||
let sc = &self.config.scoring;
|
|
||||||
match (from, to) {
|
|
||||||
(_, PileType::Foundation(_)) => sc.move_to_foundation,
|
|
||||||
(PileType::Waste, PileType::Tableau(_)) => sc.move_to_tableau,
|
|
||||||
(PileType::Foundation(_), PileType::Tableau(_)) => sc.move_from_foundation,
|
|
||||||
_ => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Score delta for exposing a face-down tableau card: +5.
|
|
||||||
pub fn score_for_flip(&self) -> i32 {
|
|
||||||
self.config.scoring.flip_up_bonus
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Score delta for undo: −15.
|
|
||||||
///
|
|
||||||
/// [`card_game::Session`] handles this via `SessionConfig::undo_penalty`
|
|
||||||
/// (default −15). We mirror the constant here so `GameState` can apply it
|
|
||||||
/// in its snapshot-based undo path without owning a `Session`.
|
|
||||||
pub const fn score_for_undo() -> i32 {
|
|
||||||
-15
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Score delta for recycling waste → stock.
|
|
||||||
///
|
|
||||||
/// [`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:
|
|
||||||
///
|
|
||||||
/// | Mode | Free recycles | Penalty per extra recycle |
|
|
||||||
/// |---|---|---|
|
|
||||||
/// | Draw-1 | 1 | −100 |
|
|
||||||
/// | Draw-3 | 3 | −20 |
|
|
||||||
///
|
|
||||||
/// `recycle_count` must be the new total **after** this recycle.
|
|
||||||
pub fn score_for_recycle(recycle_count: u32, is_draw_three: bool) -> i32 {
|
|
||||||
if is_draw_three {
|
|
||||||
if recycle_count > 3 { -20 } else { 0 }
|
|
||||||
} else if recycle_count > 1 {
|
|
||||||
-100
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ pub mod card;
|
|||||||
pub mod deck;
|
pub mod deck;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod game_state;
|
pub mod game_state;
|
||||||
pub mod klondike_adapter;
|
|
||||||
pub mod pile;
|
pub mod pile;
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
pub mod scoring;
|
pub mod scoring;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::RequestRedraw;
|
|
||||||
use solitaire_data::{AnimSpeed, Settings};
|
use solitaire_data::{AnimSpeed, Settings};
|
||||||
|
|
||||||
use crate::achievement_plugin::display_name_for;
|
use crate::achievement_plugin::display_name_for;
|
||||||
@@ -181,7 +180,6 @@ impl Plugin for AnimationPlugin {
|
|||||||
.add_message::<MoveRejectedEvent>()
|
.add_message::<MoveRejectedEvent>()
|
||||||
.add_message::<WarningToastEvent>()
|
.add_message::<WarningToastEvent>()
|
||||||
.add_message::<XpAwardedEvent>()
|
.add_message::<XpAwardedEvent>()
|
||||||
.add_message::<RequestRedraw>()
|
|
||||||
.init_resource::<EffectiveSlideDuration>()
|
.init_resource::<EffectiveSlideDuration>()
|
||||||
.init_resource::<ToastQueue>()
|
.init_resource::<ToastQueue>()
|
||||||
.init_resource::<ActiveToast>()
|
.init_resource::<ActiveToast>()
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
//! returns `None` (e.g. a transient state), the plugin retries next tick.
|
//! returns `None` (e.g. a transient state), the plugin retries next tick.
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::RequestRedraw;
|
|
||||||
|
|
||||||
use crate::audio_plugin::{AudioState, SoundLibrary};
|
use crate::audio_plugin::{AudioState, SoundLibrary};
|
||||||
use crate::events::{MoveRequestEvent, StateChangedEvent};
|
use crate::events::{MoveRequestEvent, StateChangedEvent};
|
||||||
@@ -40,9 +39,7 @@ pub struct AutoCompletePlugin;
|
|||||||
|
|
||||||
impl Plugin for AutoCompletePlugin {
|
impl Plugin for AutoCompletePlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<AutoCompleteState>()
|
app.init_resource::<AutoCompleteState>().add_systems(
|
||||||
.add_message::<RequestRedraw>()
|
|
||||||
.add_systems(
|
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
detect_auto_complete,
|
detect_auto_complete,
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ pub use timing::{
|
|||||||
pub use tuning::{AnimationTuning, InputPlatform};
|
pub use tuning::{AnimationTuning, InputPlatform};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::RequestRedraw;
|
|
||||||
|
|
||||||
use crate::card_plugin::CardEntity;
|
use crate::card_plugin::CardEntity;
|
||||||
use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, UndoRequestEvent};
|
use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, UndoRequestEvent};
|
||||||
@@ -126,7 +125,6 @@ impl Plugin for CardAnimationPlugin {
|
|||||||
.add_message::<DrawRequestEvent>()
|
.add_message::<DrawRequestEvent>()
|
||||||
.add_message::<UndoRequestEvent>()
|
.add_message::<UndoRequestEvent>()
|
||||||
.add_message::<GameWonEvent>()
|
.add_message::<GameWonEvent>()
|
||||||
.add_message::<RequestRedraw>()
|
|
||||||
.init_resource::<DragState>()
|
.init_resource::<DragState>()
|
||||||
.init_resource::<HoverState>()
|
.init_resource::<HoverState>()
|
||||||
.init_resource::<InputBuffer>()
|
.init_resource::<InputBuffer>()
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ use std::f32::consts::PI;
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::RequestRedraw;
|
|
||||||
use solitaire_core::pile::PileType;
|
use solitaire_core::pile::PileType;
|
||||||
use solitaire_data::AnimSpeed;
|
use solitaire_data::AnimSpeed;
|
||||||
|
|
||||||
@@ -205,7 +204,6 @@ impl Plugin for FeedbackAnimPlugin {
|
|||||||
.add_message::<MoveRejectedEvent>()
|
.add_message::<MoveRejectedEvent>()
|
||||||
.add_message::<NewGameRequestEvent>()
|
.add_message::<NewGameRequestEvent>()
|
||||||
.add_message::<FoundationCompletedEvent>()
|
.add_message::<FoundationCompletedEvent>()
|
||||||
.add_message::<RequestRedraw>()
|
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ fn toggle_leaderboard_screen(
|
|||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
mut requests: MessageReader<ToggleLeaderboardRequestEvent>,
|
mut requests: MessageReader<ToggleLeaderboardRequestEvent>,
|
||||||
screens: Query<Entity, With<LeaderboardScreen>>,
|
screens: Query<Entity, With<LeaderboardScreen>>,
|
||||||
other_modal_scrims: Query<(), (With<ModalScrim>, Without<LeaderboardScreen>)>,
|
|
||||||
data: Res<LeaderboardResource>,
|
data: Res<LeaderboardResource>,
|
||||||
provider: Option<Res<SyncProviderResource>>,
|
provider: Option<Res<SyncProviderResource>>,
|
||||||
settings: Option<Res<SettingsResource>>,
|
settings: Option<Res<SettingsResource>>,
|
||||||
@@ -209,11 +208,6 @@ fn toggle_leaderboard_screen(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't stack a second modal scrim over one that is already open.
|
|
||||||
if !other_modal_scrims.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn the panel immediately with whatever data we have so far.
|
// Spawn the panel immediately with whatever data we have so far.
|
||||||
let remote_available = provider
|
let remote_available = provider
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
Reference in New Issue
Block a user