Compare commits
263 Commits
bef7ab3c13
...
v0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d948fa862a | |||
| 1fcd032b0a | |||
| 3081505a3d | |||
| 07b8ecd9b2 | |||
| 5bed43ef32 | |||
| 23c9704887 | |||
| 93182fa251 | |||
| 89c51ab712 | |||
| 3984231c9b | |||
| d9f36bf34a | |||
| 57d1c58fdf | |||
| 42535f5109 | |||
| d5e6f8026b | |||
| 271647265c | |||
| 3eabc149a8 | |||
| f1aeb24157 | |||
| 000143231b | |||
| 1a1047664b | |||
| ba527de351 | |||
| fe41b502ac | |||
| b37f0cbec7 | |||
| a0fc0d2605 | |||
| 7ed4f2cba9 | |||
| ddc8f27c82 | |||
| 13dd44bd1b | |||
| 17f9b518f1 | |||
| 61d891fb76 | |||
| 7dba772e67 | |||
| ca5788f714 | |||
| 9887343d8b | |||
| 525fe0fe76 | |||
| 69ce9afab9 | |||
| 13aa0fd833 | |||
| 9f095c4039 | |||
| d8c70341f4 | |||
| 063269c70e | |||
| b126df82b2 | |||
| 655dfde736 | |||
| f712b89fe4 | |||
| f6c916641a | |||
| 95df5421c9 | |||
| fdb6c2ecfe | |||
| 9a3d7f3876 | |||
| c4970b16ea | |||
| 2c72e1fc87 | |||
| efa063fb8f | |||
| 78cf30e906 | |||
| 9a9026e33a | |||
| ab1d098877 | |||
| 160637d1c8 | |||
| 43f13c615e | |||
| 924a1e2af7 | |||
| a6b8348332 | |||
| b98cb8a99f | |||
| 7b59e70192 | |||
| 7f477b4ad8 | |||
| ce38b26721 | |||
| 172d7773f0 | |||
| 205ad6f646 | |||
| 936d035750 | |||
| 13d1d013e9 | |||
| b8fb3fbd6e | |||
| e510e90b95 | |||
| 902560cd68 | |||
| 912b08c719 | |||
| 3ef4ecb747 | |||
| 4b9d008be2 | |||
| 74482252d1 | |||
| 6e7705b256 | |||
| 59316de1e9 | |||
| 1719fdada0 | |||
| 8dda9541a3 | |||
| 60a80369d4 | |||
| dbe6c60133 | |||
| 74597a8c84 | |||
| 5d57b67934 | |||
| 220e3f040c | |||
| 54d34972d4 | |||
| 0c86cac2d5 | |||
| 2e080d02ce | |||
| 73e210b243 | |||
| f866299021 | |||
| b78a493a0c | |||
| 51d3454344 | |||
| 12789529a1 | |||
| c1bde18a2c | |||
| fd7fb7b6da | |||
| 138436558f | |||
| 65d595ad12 | |||
| abeb4e5cdf | |||
| b082bd65a6 | |||
| de52c8a7b7 | |||
| dcfa976dad | |||
| 71999e1062 | |||
| 5f5aba8dff | |||
| 9bfca929cb | |||
| 534870a68a | |||
| 0066ca6205 | |||
| 54e024c1b0 | |||
| 3a01318fbd | |||
| 79d391724e | |||
| ba019c0ba7 | |||
| 18d7c121a3 | |||
| cb93bd9265 | |||
| 6723416a55 | |||
| afb08799e8 | |||
| 3b619b8950 | |||
| 37681cf33e | |||
| 99064ce808 | |||
| de4dba6f98 | |||
| 75fc3aa3d6 | |||
| deb034c5fb | |||
| 242b5fef21 | |||
| 3f922ede28 | |||
| 8da62bd05f | |||
| 73cad7e205 | |||
| e14852c093 | |||
| 6240156fee | |||
| 1d9fb1884a | |||
| 97f38085e3 | |||
| 62cd1cf924 | |||
| b10e1a5a87 | |||
| 366fd6d127 | |||
| 7a77c66f6d | |||
| adece12cf1 | |||
| 2cfbc32715 | |||
| 56b37fc653 | |||
| 3ffde038c5 | |||
| ece2a55ffb | |||
| abda354562 | |||
| fbe984cf64 | |||
| efec6f22d5 | |||
| 7cda2a9f1a | |||
| 2b04718f33 | |||
| 505f0ebda3 | |||
| 0f40e717e1 | |||
| 08202f9351 | |||
| e22fcadb22 | |||
| 11d53245cf | |||
| f27a002c91 | |||
| ce8ba6a8c4 | |||
| 66695683eb | |||
| 18ac5adef5 | |||
| 41d75b50de | |||
| 4997356cb5 | |||
| 4bd562671e | |||
| 8221ebc803 | |||
| 4d6f8bccb7 | |||
| 800dfb50ce | |||
| 735d8766a2 | |||
| ccfeb055e5 | |||
| 8f957d919f | |||
| 2407686e13 | |||
| 1ec2593137 | |||
| ffc79447d4 | |||
| 71c0c273a1 | |||
| 21d0c289b5 | |||
| 648cd44387 | |||
| c8553dc8c5 | |||
| eedddb979e | |||
| 59a023ed5e | |||
| 8cd28cfb29 | |||
| 03227f8c77 | |||
| d387ee68d7 | |||
| 1c6094dc93 | |||
| f32e53dd0b | |||
| ddd7502a06 | |||
| c3ee7c45a7 | |||
| 4d132afdc2 | |||
| eee220fbf0 | |||
| fe23e89971 | |||
| 34f60e048a | |||
| 87fe51a0d0 | |||
| 0318480ba7 | |||
| adacc40592 | |||
| 0e7a34d6bf | |||
| 3014b65c92 | |||
| 721c17e9f8 | |||
| 60e853f52b | |||
| be4cefe79a | |||
| 74fa6c7cff | |||
| c06458cf80 | |||
| de01566e47 | |||
| 2a01ecdbfd | |||
| bf150f11f1 | |||
| 3d4d834c58 | |||
| d605fd5536 | |||
| 96ac44fbef | |||
| 2dd5b1fc9c | |||
| d0b650e08b | |||
| 9e9ce2b752 | |||
| fe986ef4a1 | |||
| fd5d488361 | |||
| e624dd26b0 | |||
| cdb1145061 | |||
| e174ed93a4 | |||
| 3eb7901023 | |||
| 91b675f2f1 | |||
| 0b0e0180c0 | |||
| bc021acfd0 | |||
| cacacb00dc | |||
| 0a76c089d0 | |||
| de840fb006 | |||
| e3ac494e85 | |||
| 11cb53ab29 | |||
| 4a33cbdc22 | |||
| dfeaed6de2 | |||
| ed0aff4714 | |||
| 46dd9cdfab | |||
| 14ef19a396 | |||
| 3d5f34a650 | |||
| 314186d6f4 | |||
| c6a596299e | |||
| 07bf1977bd | |||
| 3363da2d1a | |||
| 648c5c18d9 | |||
| 15b9b5477b | |||
| fff8c66bf7 | |||
| 299e0c6a94 | |||
| f579b96d76 | |||
| bd48813900 | |||
| 9a38873891 | |||
| 9a4071c74e | |||
| 45ef3a2058 | |||
| 6728a4311f | |||
| b37fe5b49b | |||
| d56abcd7a9 | |||
| a7b781cd36 | |||
| f7850c0075 | |||
| 00f0383867 | |||
| 20db4b312a | |||
| f7f14efe07 | |||
| 303c78aa4c | |||
| 3c01cef5f3 | |||
| 34ba4dc6ed | |||
| 13b428b81c | |||
| 9d0f9478b2 | |||
| b720588687 | |||
| adacdf533c | |||
| 7dfbff45d1 | |||
| 193410200e | |||
| 294f6fe9d4 | |||
| 788ac9f65a | |||
| 09d62f4255 | |||
| 8afb1f3fe5 | |||
| 6b793aa2ab | |||
| 0fdfbced6d | |||
| 363ddc9b75 | |||
| 0609d4eef3 | |||
| b730902d76 | |||
| 578938a9b2 | |||
| 622b35a3bf | |||
| 0cb8b32ec4 | |||
| ef043c14d4 | |||
| cfdb3b7547 | |||
| 5512a141b6 | |||
| 1f6994a084 | |||
| 4589c52368 | |||
| 82fa584cbb | |||
| b9957909b1 | |||
| 2ce11f8f4d | |||
| 5ced4c01ce | |||
| f8cce2433d |
@@ -1,4 +1,16 @@
|
||||
DATABASE_URL=sqlite://solitaire.db
|
||||
JWT_SECRET=replace_with_64_char_hex_from_openssl_rand_hex_32
|
||||
# Copy to .env and fill in the values before running docker compose up.
|
||||
|
||||
# SQLite database path inside the container.
|
||||
# When using docker-compose, leave as-is — the volume handles persistence.
|
||||
DATABASE_URL=sqlite:///data/solitaire.db
|
||||
|
||||
# HS256 signing secret for JWT tokens.
|
||||
# Generate with: openssl rand -hex 32
|
||||
JWT_SECRET=replace_me_with_a_64_char_hex_secret
|
||||
|
||||
# TCP port the server listens on inside the container.
|
||||
SERVER_PORT=8080
|
||||
ADMIN_USERNAME=admin
|
||||
|
||||
# Public domain name used by Caddy for automatic HTTPS.
|
||||
# Example: solitaire.example.com
|
||||
SOLITAIRE_DOMAIN=solitaire.example.com
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test & Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Install Linux audio/display dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libasound2-dev \
|
||||
libudev-dev \
|
||||
libwayland-dev \
|
||||
libxkbcommon-dev
|
||||
|
||||
- name: Cache cargo registry and build artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Clippy (all crates, zero warnings)
|
||||
run: cargo clippy --workspace -- -D warnings
|
||||
|
||||
- name: Test (headless crates only — no display required)
|
||||
run: |
|
||||
cargo test -p solitaire_core
|
||||
cargo test -p solitaire_sync
|
||||
cargo test -p solitaire_data
|
||||
cargo test -p solitaire_server
|
||||
|
||||
build:
|
||||
name: Release Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install Linux audio/display dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libasound2-dev \
|
||||
libudev-dev \
|
||||
libwayland-dev \
|
||||
libxkbcommon-dev
|
||||
|
||||
- name: Cache cargo registry and build artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-release-
|
||||
|
||||
- name: Build release binaries
|
||||
run: cargo build --workspace --release
|
||||
@@ -1,6 +1,9 @@
|
||||
/target
|
||||
/.sccache-cache
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
.env
|
||||
*.tmp
|
||||
data/
|
||||
.claude/
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT goal_json FROM daily_challenges WHERE date = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "goal_json",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "04d8dced0cc309bc0d968ab8f542a7f6c504ce7c2f682c0bef35dfe441ffb6e8"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id FROM users WHERE username = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2e61cd30a6cd3e0937dd096b4f94493e8bcb8c10687d0f8c0592fe38ed956fa6"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, password_hash FROM users WHERE username = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "password_hash",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2f12541a6b99acf6d9e8f7ad13438df4ab92edcbfa01f38930aae63f1554b534"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE users SET leaderboard_opt_in = 0 WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "36bab3ca8f126c66a6f4865d2699702689518bd3a796c374f932aba0434a4c94"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n r.id AS \"id!: String\",\n u.username AS \"username!: String\",\n r.seed AS \"seed!: i64\",\n r.draw_mode AS \"draw_mode!: String\",\n r.mode AS \"mode!: String\",\n r.time_seconds AS \"time_seconds!: i64\",\n r.final_score AS \"final_score!: i64\",\n r.recorded_at AS \"recorded_at!: String\",\n r.received_at AS \"received_at!: String\"\n FROM replays r\n JOIN users u ON u.id = r.user_id\n ORDER BY r.received_at DESC\n LIMIT ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: String",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "username!: String",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "seed!: i64",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "draw_mode!: String",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mode!: String",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "time_seconds!: i64",
|
||||
"ordinal": 5,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "final_score!: i64",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recorded_at!: String",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "received_at!: String",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3a9bd2e51b2389da5b7e85f26806fcffa896748e0b589d216cf60827fc3857a9"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT l.display_name, l.best_score, l.best_time_secs, l.recorded_at\n FROM leaderboard l\n JOIN users u ON u.id = l.user_id\n WHERE u.leaderboard_opt_in = 1\n ORDER BY\n CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,\n l.best_score DESC,\n CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,\n l.best_time_secs ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "display_name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "best_score",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "best_time_secs",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "recorded_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT replay_json FROM replays WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "replay_json",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5bc1984044bc792c2e9577a159ca22789469df14cb25144451f37e8cdad8165c"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE users SET leaderboard_opt_in = 1 WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5e9e61cae887a08e4224fbea43a047c093bf5a4413499bfec858e302efa91bc3"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO replays (\n id, user_id, seed, draw_mode, mode, time_seconds, final_score,\n recorded_at, received_at, replay_json\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6a36a96faa9d9b423aae3b72b0c049a1489b67ca2361581b2300bb4ee0bc9e2f"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO sync_state (user_id, stats_json, achievements_json, progress_json, last_modified)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(user_id) DO UPDATE SET\n stats_json = excluded.stats_json,\n achievements_json = excluded.achievements_json,\n progress_json = excluded.progress_json,\n last_modified = excluded.last_modified",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "720edc3b85eec90744179f2c73ed9798814b734a2f3d57405ef95772ae66a24d"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM users WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "73ffdf5be39aa5c4c160c2f77d6634a6970eeb4e1d3395f045ded747f0ce9d2a"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT leaderboard_opt_in FROM users WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "leaderboard_opt_in",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "765c87463905b2edbf37f7416d5bc38e639c7e4c5feb0f5a9d8d6fb724e7a0a8"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO leaderboard (user_id, display_name, recorded_at)\n VALUES (?, ?, ?)\n ON CONFLICT(user_id) DO UPDATE SET\n display_name = excluded.display_name,\n recorded_at = excluded.recorded_at",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8697100790ebcdf4058812cf6debad8f9a13ac91c2d0ef20cff3bd4bb9385360"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT stats_json, achievements_json, progress_json FROM sync_state WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "stats_json",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "achievements_json",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "progress_json",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a908d8d19647494f2ac2f801dc9c977f7ce23b44779da1567049856eab922645"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE leaderboard\n SET best_score = MAX(COALESCE(best_score, 0), ?),\n best_time_secs = CASE\n WHEN ? IS NULL THEN best_time_secs\n WHEN best_time_secs IS NULL THEN ?\n ELSE MIN(best_time_secs, ?)\n END,\n recorded_at = ?\n WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a931c228c46dc06a5657d419cd5dfa5a810bbce9501845c92c6619d29806d70c"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR IGNORE INTO daily_challenges (date, seed, goal_json) VALUES (?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ab537795ba39fdd28c502a8b9e615f53e7cb08f3eddd8f0574f4dc8156436de5"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO users (id, username, password_hash, created_at) VALUES (?, ?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cc1e091bf5e46e6d98baf94bac6d9ac1ca1845eff7c4b8196dd2efb93fd9e0dd"
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
# Solitaire Quest — Architecture Document
|
||||
|
||||
> **Version:** 1.1
|
||||
> **Language:** Rust (Edition 2021)
|
||||
> **Language:** Rust (Edition 2024)
|
||||
> **Engine:** Bevy (latest stable)
|
||||
> **Last Updated:** 2026-04-20
|
||||
> **Last Updated:** 2026-04-29
|
||||
|
||||
---
|
||||
|
||||
@@ -16,28 +16,25 @@
|
||||
5. [Game Engine Architecture](#5-game-engine-architecture)
|
||||
6. [Persistence & Sync Architecture](#6-persistence--sync-architecture)
|
||||
7. [Sync Server Architecture](#7-sync-server-architecture)
|
||||
8. [Google Play Games Services (Android Future)](#8-google-play-games-services-android-future)
|
||||
9. [Data Models](#9-data-models)
|
||||
10. [API Reference](#10-api-reference)
|
||||
11. [Merge Strategy](#11-merge-strategy)
|
||||
12. [Achievement System](#12-achievement-system)
|
||||
13. [Progression System](#13-progression-system)
|
||||
14. [Audio System](#14-audio-system)
|
||||
15. [Asset Pipeline](#15-asset-pipeline)
|
||||
16. [Platform Targets](#16-platform-targets)
|
||||
17. [Build & Development Guide](#17-build--development-guide)
|
||||
18. [Deployment Guide](#18-deployment-guide)
|
||||
19. [Security Model](#19-security-model)
|
||||
20. [Testing Strategy](#20-testing-strategy)
|
||||
21. [Decision Log](#21-decision-log)
|
||||
8. [Data Models](#8-data-models)
|
||||
9. [API Reference](#9-api-reference)
|
||||
10. [Merge Strategy](#10-merge-strategy)
|
||||
11. [Achievement System](#11-achievement-system)
|
||||
12. [Progression System](#12-progression-system)
|
||||
13. [Audio System](#13-audio-system)
|
||||
14. [Asset Pipeline](#14-asset-pipeline)
|
||||
15. [Platform Targets](#15-platform-targets)
|
||||
16. [Build & Development Guide](#16-build--development-guide)
|
||||
17. [Deployment Guide](#17-deployment-guide)
|
||||
18. [Security Model](#18-security-model)
|
||||
19. [Testing Strategy](#19-testing-strategy)
|
||||
20. [Decision Log](#20-decision-log)
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops (iOS/Android as a stretch goal). It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
|
||||
|
||||
On Android (stretch goal), sync is enhanced with Google Play Games Services (GPGS) for native achievement popups, leaderboards, and cloud saves — sitting on top of the same underlying sync payload so data stays consistent regardless of which backend was used last.
|
||||
Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops. It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
|
||||
|
||||
### Sync Backend by Platform
|
||||
|
||||
@@ -46,8 +43,6 @@ On Android (stretch goal), sync is enhanced with Google Play Games Services (GPG
|
||||
| macOS | Self-hosted server | Full feature set |
|
||||
| Windows | Self-hosted server | Full feature set |
|
||||
| Linux | Self-hosted server | Full feature set |
|
||||
| Android (stretch) | Google Play Games Services | + server as fallback |
|
||||
| iOS (stretch) | Self-hosted server | GPGS not supported on iOS |
|
||||
|
||||
### Design Principles
|
||||
|
||||
@@ -56,6 +51,7 @@ On Android (stretch goal), sync is enhanced with Google Play Games Services (GPG
|
||||
- **No panics in game logic.** Every state transition returns `Result<_, MoveError>`. Panics are only acceptable in startup/configuration code.
|
||||
- **One language, one repo.** The game client, sync client, shared types, and sync server are all Rust crates in a single Cargo workspace.
|
||||
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
|
||||
- **UI-first interaction.** Every player-triggered action — new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, etc. — must be reachable from a visible UI control. Keyboard shortcuts exist only as optional accelerators for power users; they are never the sole entry point. A player using only mouse or touch must be able to perform every action. New gameplay features ship with the UI control alongside the system that backs it.
|
||||
|
||||
---
|
||||
|
||||
@@ -72,26 +68,25 @@ solitaire_quest/
|
||||
├── Dockerfile # Multi-stage server build
|
||||
├── docker-compose.yml # Server + Caddy reverse proxy
|
||||
│
|
||||
├── assets/ # All runtime assets (loaded via Bevy AssetServer)
|
||||
├── assets/ # Loaded at runtime via AssetServer (audio is embedded via include_bytes!())
|
||||
│ ├── cards/
|
||||
│ │ ├── faces/ # Card face sprites (suit + rank)
|
||||
│ │ └── backs/ # Card back designs (back_0.png … back_4.png)
|
||||
│ ├── backgrounds/ # Table backgrounds (bg_0.png … bg_4.png)
|
||||
│ ├── fonts/ # .ttf font files
|
||||
│ │ ├── faces/{RANK}{SUIT}.png # 52 card faces — rendered from hayeah/playing-cards-assets SVGs (MIT)
|
||||
│ │ └── backs/back_0.png – back_4.png # back_0 = generated default back; back_1–4 are generated patterns
|
||||
│ ├── backgrounds/bg_0.png – bg_4.png # generated textures
|
||||
│ ├── fonts/main.ttf # FiraMono-Medium (170K, OFL)
|
||||
│ └── audio/
|
||||
│ ├── card_deal.ogg
|
||||
│ ├── card_flip.ogg
|
||||
│ ├── card_place.ogg
|
||||
│ ├── card_invalid.ogg
|
||||
│ ├── win_fanfare.ogg
|
||||
│ └── ambient_loop.ogg
|
||||
│ ├── card_deal.wav
|
||||
│ ├── card_flip.wav
|
||||
│ ├── card_place.wav
|
||||
│ ├── card_invalid.wav
|
||||
│ ├── win_fanfare.wav
|
||||
│ └── ambient_loop.wav
|
||||
│
|
||||
├── solitaire_core/ # Pure Rust game logic — zero external deps beyond rand/serde
|
||||
├── solitaire_sync/ # Shared API types — used by client and server
|
||||
├── solitaire_data/ # Persistence, sync client, settings
|
||||
├── solitaire_engine/ # Bevy ECS systems, components, plugins
|
||||
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite)
|
||||
├── solitaire_gpgs/ # Google Play Games Services bridge (Android only, stub until stretch goal)
|
||||
└── solitaire_app/ # Main binary entry point
|
||||
```
|
||||
|
||||
@@ -135,25 +130,10 @@ Owns:
|
||||
- `SyncBackend` enum and backend selection
|
||||
- Solitaire Server sync client (JWT auth, auto-refresh)
|
||||
- OS keychain integration (`keyring`)
|
||||
- `SyncProvider` trait — implemented by both `SolitaireServerClient` and `GpgsClient` (Android)
|
||||
|
||||
### `solitaire_gpgs` *(stub — implement when targeting Android)*
|
||||
**Dependencies:** `solitaire_sync`, `jni` (Android only), `solitaire_data` trait impls.
|
||||
|
||||
Android-only crate, compiled only when `target_os = "android"`. Bridges the Google Play Games Services Java SDK via JNI.
|
||||
|
||||
Owns:
|
||||
- `GpgsClient` implementing the `SyncProvider` trait from `solitaire_data`
|
||||
- GPGS Saved Games API calls (load/save cloud save slot)
|
||||
- GPGS Achievements API calls (unlock, reveal, increment)
|
||||
- GPGS Leaderboards API calls (submit score, load scores)
|
||||
- Google Sign-In token management (via JNI into Android SDK)
|
||||
- Conversion between GPGS cloud save blob ↔ `SyncPayload`
|
||||
|
||||
> **Note:** This crate contains only a trait stub and compile-time stub implementations until Android support is actively developed. Do not implement JNI bindings until Phase: Android.
|
||||
- `SyncProvider` trait — implemented by `SolitaireServerClient`
|
||||
|
||||
### `solitaire_engine`
|
||||
**Dependencies:** `bevy`, `bevy_egui`, `bevy_kira_audio`, `solitaire_core`, `solitaire_data`.
|
||||
**Dependencies:** `bevy`, `kira`, `solitaire_core`, `solitaire_data`.
|
||||
|
||||
All Bevy-specific code. Structured as a collection of Plugins that `solitaire_app` registers.
|
||||
|
||||
@@ -162,9 +142,10 @@ Owns:
|
||||
- Rendering systems (card sprites, table, backgrounds)
|
||||
- Drag-and-drop input handling
|
||||
- Animation systems (slide, flip, win cascade, toast)
|
||||
- All egui screens (Home, Stats, Achievements, Settings, Profile)
|
||||
- All Bevy UI screens (Home, Stats, Achievements, Settings, Profile)
|
||||
- Audio playback systems
|
||||
- Sync status display
|
||||
- Card, background, and font asset loading via Bevy `AssetServer` (audio is the lone exception — embedded via `include_bytes!()` in `audio_plugin.rs`)
|
||||
|
||||
### `solitaire_server`
|
||||
**Dependencies:** `solitaire_sync`, `axum`, `sqlx`, `jsonwebtoken`, `bcrypt`, `tower-governor`, `tracing`, `tokio`, `dotenvy`.
|
||||
@@ -209,7 +190,7 @@ RenderSystem ScoreSystem AchievementSystem
|
||||
│
|
||||
│ fires AchievementUnlockedEvent
|
||||
▼
|
||||
ToastSystem (egui popup)
|
||||
ToastSystem (Bevy UI popup)
|
||||
PersistenceSystem (write to disk)
|
||||
```
|
||||
|
||||
@@ -223,8 +204,7 @@ SyncPlugin::on_startup()
|
||||
│ spawns AsyncComputeTask
|
||||
▼
|
||||
solitaire_data::sync_pull() ← dispatches to active SyncProvider
|
||||
│ SolitaireServerClient (desktop / iOS)
|
||||
│ GpgsClient (Android, future)
|
||||
│ SolitaireServerClient
|
||||
▼
|
||||
solitaire_sync::merge(local, remote)
|
||||
│
|
||||
@@ -245,7 +225,7 @@ SyncPlugin::on_exit()
|
||||
│ blocking push (acceptable on exit, not on main loop)
|
||||
▼
|
||||
active SyncProvider::push(local)
|
||||
│ POST to server — or — GPGS Saved Games PUT (Android)
|
||||
│ POST to server
|
||||
▼
|
||||
Done
|
||||
```
|
||||
@@ -256,16 +236,38 @@ Done
|
||||
|
||||
### Bevy Plugins
|
||||
|
||||
| Plugin | Responsibility |
|
||||
|---|---|
|
||||
| `CardPlugin` | Card entity spawning, sprite management, drag-and-drop |
|
||||
| `TablePlugin` | Pile markers, background, layout calculation |
|
||||
| `AnimationPlugin` | Slide, flip, win cascade, toast animations |
|
||||
| `AudioPlugin` | Sound effect and music playback via bevy_kira_audio |
|
||||
| `UIPlugin` | All egui screens: Home, Stats, Achievements, Settings, Profile |
|
||||
| `AchievementPlugin` | Listens for game events, evaluates unlock conditions, fires toasts |
|
||||
| `SyncPlugin` | Manages sync lifecycle (pull on start, push on exit, status display) |
|
||||
| `GamePlugin` | Core game state resource, input routing, win detection |
|
||||
The "Shortcut" column lists optional keyboard accelerators. Every action in this table must also be reachable from a visible UI control (button, menu item, on-screen affordance) per the UI-first design principle in §1; the shortcut is a power-user convenience, not the sole entry point.
|
||||
|
||||
| Plugin | Shortcut | Responsibility |
|
||||
|---|---|---|
|
||||
| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop |
|
||||
| `TablePlugin` | — | Pile markers, background, layout calculation |
|
||||
| `FontPlugin` | — | Loads FiraMono-Medium via `AssetServer` at startup; exposes `FontResource` handle |
|
||||
| `AnimationPlugin` | — | Slide, flip, win cascade, toast animations |
|
||||
| `FeedbackAnimPlugin` | — | Shake, settle, and deal-stagger animations |
|
||||
| `AutoCompletePlugin` | Enter | Executes auto-complete when the HUD badge is lit |
|
||||
| `AudioPlugin` | — | Sound effect and music playback via kira |
|
||||
| `InputPlugin` | — | Keyboard and mouse input routing |
|
||||
| `CursorPlugin` | — | Custom cursor sprite during drag |
|
||||
| `SelectionPlugin` | — | Keyboard-driven card selection |
|
||||
| `GamePlugin` | N | Core game state resource, new-game flow, win/game-over overlays |
|
||||
| `HudPlugin` | — | Score, move counter, timer, auto-complete badge, and the top-right action button bar (Undo / Pause / Help / New Game). Each button fires the same request event the corresponding hotkey does. |
|
||||
| `StatsPlugin` | S | Stats overlay and persistence |
|
||||
| `ProgressPlugin` | — | XP/level system, persistence |
|
||||
| `AchievementPlugin` | A | Unlock evaluation, toast events, persistence |
|
||||
| `DailyChallengePlugin` | — | Daily challenge resource and completion tracking |
|
||||
| `WeeklyGoalsPlugin` | — | Weekly goal progress and completion events |
|
||||
| `ChallengePlugin` | — | Challenge mode progression (seeded hard deals) |
|
||||
| `TimeAttackPlugin` | — | 10-minute time-attack mode timer |
|
||||
| `HomePlugin` | M | Main-menu overlay with keyboard shortcut reference |
|
||||
| `ProfilePlugin` | P | Player profile overlay: level, XP, achievements, sync status |
|
||||
| `SettingsPlugin` | O | Settings panel: audio, draw mode, theme, sync, cosmetics |
|
||||
| `LeaderboardPlugin` | L | Leaderboard overlay |
|
||||
| `HelpPlugin` | H | Help / controls overlay |
|
||||
| `PausePlugin` | Esc | Pause and resume |
|
||||
| `OnboardingPlugin` | — | First-run welcome screen |
|
||||
| `SyncPlugin` | — | Async sync lifecycle (pull on start, push on exit, status display) |
|
||||
| `WinSummaryPlugin` | — | Win cascade overlay and screen-shake effect |
|
||||
|
||||
### Key Bevy Resources
|
||||
|
||||
@@ -290,6 +292,20 @@ struct StatsResource(StatsSnapshot);
|
||||
struct ProgressResource(PlayerProgress);
|
||||
struct AchievementsResource(Vec<AchievementRecord>);
|
||||
struct SettingsResource(Settings);
|
||||
|
||||
// Pre-loaded card face and back PNG handles
|
||||
struct CardImageSet {
|
||||
faces: [[Handle<Image>; 13]; 4], // [suit][rank]: Clubs=0..Spades=3, Ace=0..King=12
|
||||
backs: [Handle<Image>; 5], // indexed by selected_card_back setting
|
||||
}
|
||||
|
||||
// Project-wide font handle (FiraMono-Medium loaded via AssetServer at startup)
|
||||
struct FontResource(Handle<Font>);
|
||||
|
||||
// Pre-loaded background PNG handles
|
||||
struct BackgroundImageSet {
|
||||
handles: Vec<Handle<Image>>, // indices 0–4 match selected_background setting
|
||||
}
|
||||
```
|
||||
|
||||
### Key Bevy Events
|
||||
@@ -363,7 +379,6 @@ Implementations:
|
||||
|---|---|---|
|
||||
| `LocalOnlyProvider` | No-op (default) | All |
|
||||
| `SolitaireServerClient` | Self-hosted server | All |
|
||||
| `GpgsClient` *(future)* | Google Play Games Services | Android only |
|
||||
|
||||
Sync always runs on `bevy::tasks::AsyncComputeTaskPool` — the game thread is never blocked.
|
||||
|
||||
@@ -378,9 +393,6 @@ pub enum SyncBackend {
|
||||
// JWT access + refresh tokens stored in OS keychain
|
||||
// key: "solitaire_quest_server_{username}"
|
||||
},
|
||||
GooglePlayGames,
|
||||
// No credentials stored locally — auth managed by Google Sign-In SDK via JNI
|
||||
// Android only; selecting this on non-Android falls back to Local silently
|
||||
}
|
||||
```
|
||||
|
||||
@@ -392,10 +404,6 @@ On exit: `POST /api/sync/push` with payload
|
||||
On 401: automatically attempt `POST /api/auth/refresh`, retry once, then surface error to user.
|
||||
Credentials stored in OS keychain via `keyring` — never in plaintext on disk.
|
||||
|
||||
### Google Play Games Sync *(Android — future, see Section 8)*
|
||||
|
||||
Implemented in `solitaire_gpgs` crate. Uses the GPGS Saved Games API with named slot `"solitaire_quest_sync"`. The `GpgsClient` struct implements `SyncProvider` — the `SyncPlugin` treats it identically to `SolitaireServerClient`. The same `solitaire_sync::merge()` function applies regardless of which provider returned the remote data.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sync Server Architecture
|
||||
@@ -482,89 +490,7 @@ This ensures all players worldwide get the same challenge for a given date, rega
|
||||
|
||||
---
|
||||
|
||||
## 8. Google Play Games Services (Android Future)
|
||||
|
||||
> **Status: Stub only.** Do not implement JNI bindings until Android is actively targeted. The `solitaire_gpgs` crate exists in the workspace with a trait stub so the compiler enforces the interface contract from day one.
|
||||
|
||||
### Why GPGS on Android
|
||||
|
||||
Google Play Games Services provides first-class Android features that would otherwise require significant backend work:
|
||||
|
||||
| Feature | GPGS Provides | Our Alternative |
|
||||
|---|---|---|
|
||||
| Cloud saves | Saved Games API | Self-hosted server |
|
||||
| Achievements | Native popups + Play profile | In-game toasts only |
|
||||
| Leaderboards | Hosted by Google, visible in Play app | Server leaderboard |
|
||||
| Auth | Google Sign-In, no registration | Username + password |
|
||||
|
||||
On Android, GPGS is the **primary** sync provider. The self-hosted server is the **fallback** if the player is not signed in or has no server configured. Both can be active simultaneously — a win pushes to both, pull merges from whichever responded last.
|
||||
|
||||
### Compatibility Reality
|
||||
|
||||
| Platform | GPGS Support |
|
||||
|---|---|
|
||||
| Android | ✅ Full |
|
||||
| Windows | ✅ GPGS for PC (optional, separate SDK) |
|
||||
| macOS | ❌ Not supported |
|
||||
| Linux | ❌ Not supported |
|
||||
| iOS | ❌ Not supported |
|
||||
|
||||
macOS, Linux, and iOS users always use the self-hosted server. This is why the server is the primary design and GPGS is an enhancement layer.
|
||||
|
||||
### `solitaire_gpgs` Crate Design
|
||||
|
||||
The crate is compiled only on Android (`#[cfg(target_os = "android")]`). On all other platforms the crate exports only the stub.
|
||||
|
||||
```rust
|
||||
// solitaire_gpgs/src/lib.rs
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod stub;
|
||||
|
||||
pub use stub::GpgsClient; // stub on desktop
|
||||
#[cfg(target_os = "android")]
|
||||
pub use android::GpgsClient; // real impl on Android
|
||||
```
|
||||
|
||||
### JNI Bridge (Android implementation — future)
|
||||
|
||||
The real `GpgsClient` uses the `jni` crate to call into the GPGS Android SDK:
|
||||
|
||||
```
|
||||
Rust GpgsClient
|
||||
│ jni::JNIEnv
|
||||
▼
|
||||
Java: com.google.android.gms.games.PlayGames
|
||||
├── getSnapshotsClient() → Saved Games (sync payload)
|
||||
├── getAchievementsClient() → unlock / reveal
|
||||
└── getLeaderboardsClient() → submit score
|
||||
```
|
||||
|
||||
Steps required when Android work begins:
|
||||
1. Add `cargo-mobile2` to the build toolchain
|
||||
2. Implement `GpgsClient` with `jni` bindings in `solitaire_gpgs/src/android.rs`
|
||||
3. Add `GpgsClient: SyncProvider` impl — pull/push map to Saved Games load/save
|
||||
4. Mirror achievement unlocks: on `AchievementUnlockedEvent`, call GPGS unlock API alongside in-game toast
|
||||
5. Submit scores to GPGS leaderboard on `GameWonEvent`
|
||||
6. Add Google Sign-In button to the Settings screen (Android build only, `#[cfg]` gated)
|
||||
|
||||
### Dual-Sync on Android
|
||||
|
||||
When both GPGS and the self-hosted server are configured, the `SyncPlugin` runs both providers concurrently and merges all three payloads (local + GPGS + server) using the same `solitaire_sync::merge()` function applied twice:
|
||||
|
||||
```
|
||||
local ──────┐
|
||||
├── merge() ──► intermediate ──┐
|
||||
gpgs ────────┘ ├── merge() ──► final
|
||||
server ──────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Data Models
|
||||
## 8. Data Models
|
||||
|
||||
### Core Game Models (`solitaire_core`)
|
||||
|
||||
@@ -588,6 +514,9 @@ pub enum PileType {
|
||||
|
||||
pub enum DrawMode { DrawOne, DrawThree }
|
||||
|
||||
/// Active game mode. Classic is the default; others unlock at level 5.
|
||||
pub enum GameMode { Classic, Zen, Challenge, TimeAttack }
|
||||
|
||||
pub enum MoveError {
|
||||
InvalidSource,
|
||||
InvalidDestination,
|
||||
@@ -600,13 +529,16 @@ pub enum MoveError {
|
||||
pub struct GameState {
|
||||
pub piles: HashMap<PileType, Vec<Card>>,
|
||||
pub draw_mode: DrawMode,
|
||||
pub mode: GameMode,
|
||||
pub score: i32,
|
||||
pub move_count: u32,
|
||||
pub undo_count: u32, // number of undos used in this game
|
||||
pub recycle_count: u32, // number of stock recycles
|
||||
pub elapsed_seconds: u64,
|
||||
pub seed: u64,
|
||||
pub is_won: bool,
|
||||
pub is_auto_completable: bool,
|
||||
undo_stack: Vec<StateSnapshot>, // private, max 64
|
||||
undo_stack: VecDeque<StateSnapshot>, // private, max 64 (VecDeque for O(1) pop_front)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -652,14 +584,14 @@ pub struct Settings {
|
||||
pub music_volume: f32,
|
||||
pub animation_speed: AnimSpeed,
|
||||
pub theme: Theme,
|
||||
pub sync_backend: SyncBackend, // Local | SolitaireServer | GooglePlayGames
|
||||
pub sync_backend: SyncBackend, // Local | SolitaireServer
|
||||
pub first_run_complete: bool,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. API Reference
|
||||
## 9. API Reference
|
||||
|
||||
All endpoints are under the base URL configured by the user (e.g., `https://solitaire.example.com`).
|
||||
|
||||
@@ -702,9 +634,9 @@ All endpoints are under the base URL configured by the user (e.g., `https://soli
|
||||
|
||||
---
|
||||
|
||||
## 11. Merge Strategy
|
||||
## 10. Merge Strategy
|
||||
|
||||
Used identically by the `SolitaireServerClient`, `GpgsClient`, and server-side handler. Lives in `solitaire_sync` as a pure function with no I/O. Called once per provider when multiple backends are active simultaneously (e.g. GPGS + server on Android).
|
||||
Used by both `SolitaireServerClient` and the server-side handler. Lives in `solitaire_sync` as a pure function with no I/O.
|
||||
|
||||
```rust
|
||||
pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
||||
@@ -744,7 +676,7 @@ pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
||||
|
||||
---
|
||||
|
||||
## 12. Achievement System
|
||||
## 11. Achievement System
|
||||
|
||||
### Definition Structure
|
||||
|
||||
@@ -789,13 +721,9 @@ pub struct AchievementDef {
|
||||
|
||||
Achievement conditions are evaluated by `AchievementPlugin` on every `GameWonEvent` and `StateChangedEvent`. The plugin calls `solitaire_core::check_achievements()` which returns a `Vec<AchievementDef>` of newly unlocked achievements. The plugin then fires `AchievementUnlockedEvent` for each, which the toast and persistence systems handle independently.
|
||||
|
||||
### GPGS Mirroring *(Android, future)*
|
||||
|
||||
When the `GpgsClient` is active, every `AchievementUnlockedEvent` also triggers a GPGS `achievements.unlock()` call via JNI so the achievement appears in the player's Google Play profile. A static map in `solitaire_gpgs` maps our achievement IDs to GPGS achievement IDs (assigned in the Google Play Console). Mirroring is fire-and-forget — failures are logged but never block the in-game toast.
|
||||
|
||||
---
|
||||
|
||||
## 13. Progression System
|
||||
## 12. Progression System
|
||||
|
||||
### XP Sources
|
||||
|
||||
@@ -824,62 +752,85 @@ Levels 11+: level = 10 + floor((total_xp - 5000) / 1000)
|
||||
|
||||
---
|
||||
|
||||
## 14. Audio System
|
||||
## 13. Audio System
|
||||
|
||||
Audio uses `bevy_kira_audio`. All sound files are `.ogg` (good compression, cross-platform, royalty-free).
|
||||
Audio uses `kira`. All sound files are `.wav`.
|
||||
|
||||
| File | Trigger |
|
||||
|---|---|
|
||||
| `card_deal.ogg` | New game deal animation |
|
||||
| `card_flip.ogg` | Card flips face-up |
|
||||
| `card_place.ogg` | Valid card placement |
|
||||
| `card_invalid.ogg` | Invalid move attempt |
|
||||
| `win_fanfare.ogg` | Game won |
|
||||
| `ambient_loop.ogg` | Looping background music (restarts seamlessly) |
|
||||
| `card_deal.wav` | New game deal animation |
|
||||
| `card_flip.wav` | Card flips face-up |
|
||||
| `card_place.wav` | Valid card placement |
|
||||
| `card_invalid.wav` | Invalid move attempt |
|
||||
| `win_fanfare.wav` | Game won |
|
||||
| `ambient_loop.wav` | Looping background music |
|
||||
|
||||
Volume is controlled by two independent sliders in Settings (`sfx_volume`, `music_volume`), each stored in `Settings` and applied as `bevy_kira_audio` channel volumes.
|
||||
Volume is controlled by two independent sliders in Settings (`sfx_volume`, `music_volume`), each stored in `Settings` and applied as `kira` channel volumes.
|
||||
|
||||
Audio systems listen for Bevy events and never block the game thread.
|
||||
|
||||
---
|
||||
|
||||
## 15. Asset Pipeline
|
||||
## 14. Asset Pipeline
|
||||
|
||||
All assets are loaded through Bevy's `AssetServer`. No bytes are hardcoded in source.
|
||||
### Rendering approach
|
||||
|
||||
### Card Sprites
|
||||
Cards are Bevy `Sprite` entities with `Handle<Image>` from `CardImageSet`. Face-up cards use one of 52 individual face PNGs selected by `faces[suit][rank]` — rank and suit are baked into each image and no `Text2d` overlay is spawned. Face-down cards use `backs/back_N.png` indexed by `settings.selected_card_back`. `Text2d` labels are only used as a fallback when `CardImageSet` is absent (e.g. tests with `MinimalPlugins`). `CardImageSet` is populated at startup by `card_plugin::load_card_images` via `AssetServer::load()`.
|
||||
|
||||
Card faces can be either:
|
||||
- A texture atlas (`assets/cards/atlas.png` + `atlas.ron` layout) — faster to load, preferred
|
||||
- Individual files (`assets/cards/faces/2_of_clubs.png`, etc.) — easier to author
|
||||
Backgrounds are Bevy `Sprite` entities with `Handle<Image>` from `BackgroundImageSet`. `BackgroundImageSet` is populated at startup by `table_plugin::load_background_images` via `AssetServer::load()`.
|
||||
|
||||
Card backs: `assets/cards/backs/back_0.png` through `back_4.png`. Additional backs unlocked via achievements are in the same folder, gated by `PlayerProgress::unlocked_card_backs`.
|
||||
The font `FiraMono-Medium` is loaded via `AssetServer::load("fonts/main.ttf")` at startup by `FontPlugin` and exposed as `FontResource` for use by all UI and text systems.
|
||||
|
||||
### Backgrounds
|
||||
All three loaders take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` in tests: when the server is absent, `CardImageSet`/`BackgroundImageSet` are inserted with empty handle slots and the plugins fall back to `Text2d` rank+suit overlays and solid-colour board backgrounds. The `assets/` directory must ship alongside the binary.
|
||||
|
||||
`assets/backgrounds/bg_0.png` through `bg_4.png`. Same unlock gating as card backs.
|
||||
The `assets/` directory layout:
|
||||
|
||||
### Fonts
|
||||
```
|
||||
assets/
|
||||
├── cards/
|
||||
│ ├── faces/{rank}_{suit}.png # 52 individual card faces (120×168, generated by solitaire_assetgen)
|
||||
│ └── backs/back_0.png – back_4.png # placeholder patterns
|
||||
├── backgrounds/bg_0.png – bg_4.png # placeholder textures
|
||||
├── fonts/main.ttf # FiraMono-Medium (170K, OFL)
|
||||
└── audio/
|
||||
├── card_deal.wav
|
||||
├── card_flip.wav
|
||||
├── card_place.wav
|
||||
├── card_invalid.wav
|
||||
├── win_fanfare.wav
|
||||
└── ambient_loop.wav
|
||||
```
|
||||
|
||||
`assets/fonts/main.ttf` — used for card rank/suit text and all egui overrides.
|
||||
### Audio
|
||||
|
||||
All sound effect WAV files are embedded at compile time via `include_bytes!()` in `audio_plugin.rs`. There is no runtime asset loading — the binary is fully self-contained.
|
||||
|
||||
| File | Trigger |
|
||||
|---|---|
|
||||
| `card_deal.wav` | New game deal animation |
|
||||
| `card_flip.wav` | Card flips face-up |
|
||||
| `card_place.wav` | Valid card placement |
|
||||
| `card_invalid.wav` | Invalid move attempt |
|
||||
| `win_fanfare.wav` | Game won |
|
||||
| `ambient_loop.wav` | Looping background music |
|
||||
|
||||
---
|
||||
|
||||
## 16. Platform Targets
|
||||
## 15. Platform Targets
|
||||
|
||||
| Platform | Status | Primary Sync | Notes |
|
||||
|---|---|---|---|
|
||||
| macOS | Primary | Self-hosted server | x86_64 + Apple Silicon (universal binary via `cargo-lipo`) |
|
||||
| Windows | Primary | Self-hosted server | x86_64, MSVC toolchain; optional GPGS for PC (future) |
|
||||
| Windows | Primary | Self-hosted server | x86_64, MSVC toolchain |
|
||||
| Linux | Primary | Self-hosted server | x86_64, tested on Ubuntu 22.04+ and Fedora 39+ |
|
||||
| Android | Stretch | Google Play Games + server | `cargo-mobile2`, touch input, GPGS via JNI |
|
||||
| iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input; GPGS unavailable on iOS |
|
||||
| Android | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
||||
| iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
||||
|
||||
Minimum Bevy window size enforced: 800×600. Desktop windows are freely resizable; layout recomputes on `WindowResized`.
|
||||
|
||||
---
|
||||
|
||||
## 17. Build & Development Guide
|
||||
## 16. Build & Development Guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -940,7 +891,7 @@ Add `--features bevy/dynamic_linking` during development to dramatically reduce
|
||||
|
||||
---
|
||||
|
||||
## 18. Deployment Guide
|
||||
## 17. Deployment Guide
|
||||
|
||||
### Docker Compose (Recommended)
|
||||
|
||||
@@ -985,7 +936,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
|
||||
|
||||
---
|
||||
|
||||
## 19. Security Model
|
||||
## 18. Security Model
|
||||
|
||||
| Concern | Mitigation |
|
||||
|---|---|
|
||||
@@ -1001,7 +952,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
|
||||
|
||||
---
|
||||
|
||||
## 20. Testing Strategy
|
||||
## 19. Testing Strategy
|
||||
|
||||
### Unit Tests (`solitaire_core`)
|
||||
|
||||
@@ -1040,12 +991,10 @@ Using `axum::test` and an in-memory SQLite database:
|
||||
- [ ] Achievement toast appears and dismisses
|
||||
- [ ] Server sync: register, login, push, pull on second machine
|
||||
- [ ] Server sync: JWT refresh on 401 works transparently
|
||||
- [ ] GPGS sync (Android only): sign in, unlock achievement, verify appears in Play Games app
|
||||
- [ ] Dual sync (Android only): GPGS + server both configured, payloads merge correctly
|
||||
|
||||
---
|
||||
|
||||
## 21. Decision Log
|
||||
## 20. Decision Log
|
||||
|
||||
| Decision | Rationale | Date |
|
||||
|---|---|---|
|
||||
@@ -1057,7 +1006,10 @@ Using `axum::test` and an in-memory SQLite database:
|
||||
| bcrypt cost 12 | Balances security and registration latency (~300ms on modern hardware); higher than default 10 | 2026-04-19 |
|
||||
| No email required for server accounts | Reduces PII collected; simplifies self-hosted deployments; password reset handled by server admin if needed | 2026-04-19 |
|
||||
| Self-hosted server as primary sync (not WebDAV) | A proper Rust server gives us auth, leaderboards, and daily challenge seeding for minimal extra effort over WebDAV, and removes a redundant backend | 2026-04-20 |
|
||||
| `SyncProvider` trait, not `SyncBackend` match arms | Allows adding Google Play Games Services cleanly; `SyncPlugin` stays backend-agnostic and testable | 2026-04-20 |
|
||||
| GPGS as Android enhancement, not replacement | GPGS has no macOS/Linux support; the server must remain universal, with GPGS layered on top for Android players | 2026-04-20 |
|
||||
| `SyncProvider` trait, not `SyncBackend` match arms | `SyncPlugin` stays backend-agnostic and testable; new backends can be added without touching the plugin | 2026-04-20 |
|
||||
| Dropped WebDAV backend | Redundant once the self-hosted server exists; removing it reduces surface area and simplifies settings UI | 2026-04-20 |
|
||||
| `solitaire_gpgs` crate stubbed from day one | Enforces the `SyncProvider` interface contract at compile time even before Android work begins; avoids architectural rework later | 2026-04-20 |
|
||||
| Dropped GPGS backend | Redundant with the self-hosted server; adds JNI complexity for no user-visible benefit on the target platforms | 2026-04-28 |
|
||||
| Card, background, and font assets loaded via `AssetServer` | Reverses the earlier embed-via-`include_bytes!()` decision: PNGs and TTFs are loaded at runtime so artwork can be swapped (e.g. alternate card backs, themed backgrounds) without a recompile, and binary size stays small. Loaders take `Option<Res<AssetServer>>` and fall back gracefully under `MinimalPlugins`. The `assets/` directory must ship alongside the binary. | 2026-04-29 |
|
||||
| Audio assets remain embedded via `include_bytes!()` | Audio files are small, change rarely, and the embedded path eliminates a class of runtime-load errors during gameplay; the asset-pipeline reversal does not extend to audio | 2026-04-29 |
|
||||
| Card art swapped from xCards (LGPL-3.0) to hayeah/playing-cards-assets (MIT) | Public-release readiness. The previous xCards art carried LGPL relinking obligations that complicate a single-binary distribution; hayeah's set derives from the public-domain `vector-playing-cards` line-art and is permissively MIT-licensed. CREDITS.md license summary collapsed to MIT + OFL-1.1. The default card back is original work in this project's midnight-purple palette. | 2026-05-01 |
|
||||
| Runtime SVG card-theme system (`CARD_PLAN.md`) | User-supplied themes need to ship SVG sources so they can rasterise at any resolution on the player's hardware; baking PNGs at build time only would lock theme installation to the developer. The pipeline (usvg → resvg → tiny-skia) rasterises once per (theme, target size) at load time and caches the resulting `Image`, so the runtime cost is paid once, not per frame. The bundled default theme ships via `embedded://`; user themes via `themes://` rooted at `user_theme_dir()`. | 2026-05-01 |
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Solitaire Quest are documented here. The format is
|
||||
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
|
||||
project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
_Nothing yet._
|
||||
|
||||
## [0.14.0] — 2026-05-02
|
||||
|
||||
Two threads land in v0.14.0: the second half of the post-v0.12.0 UX
|
||||
candidate list (theme thumbnails, daily-challenge calendar, Time Attack
|
||||
auto-save, per-mode bests, time-bonus multiplier) plus a **major new
|
||||
feature** — the replay pipeline (record → upload → web viewer). Three
|
||||
Quat-reported bugs from a smoke-test round shipped alongside.
|
||||
|
||||
### Added
|
||||
|
||||
- **Theme-picker thumbnails** in Settings → Cosmetic. Each theme chip
|
||||
renders a small Ace-of-Spades + back preview pair via the existing
|
||||
`rasterize_svg` path. Cached per theme in a new
|
||||
`ThemeThumbnailCache`. Themes that lack a preview SVG fall back to
|
||||
a transparent placeholder rather than crashing.
|
||||
- **14-day daily-challenge calendar** in the Profile modal. Horizontal
|
||||
row of dots showing the trailing two weeks; today's dot is ringed
|
||||
in `ACCENT_PRIMARY`, completed days fill `STATE_SUCCESS`, missed
|
||||
days fill `BG_ELEVATED`. Caption above the row reads "Current
|
||||
streak: N · Longest: M".
|
||||
- **Time Attack session auto-save** to `<data_dir>/time_attack_session.json`,
|
||||
atomic .tmp + rename. 30-second auto-save while a session is active,
|
||||
plus on `AppExit`. Sessions whose 10-minute window expired in real
|
||||
time while the app was closed are discarded on load. Classic, Zen,
|
||||
and Challenge already auto-saved correctly via `game_state.json` —
|
||||
Time Attack was the only mode missing session-level persistence.
|
||||
- **Per-mode best-score and fastest-win readouts** in the Stats screen.
|
||||
`StatsSnapshot` gains six `#[serde(default)]` fields (Classic / Zen
|
||||
/ Challenge × best_score + fastest_win_seconds). Stats screen renders
|
||||
a "Per-mode bests" section between the primary cell grid and
|
||||
progression. Lifetime totals continue to roll all modes together.
|
||||
- **Time-bonus multiplier slider** in Settings → Gameplay (0.0–2.0,
|
||||
0.1 steps, default 1.0, "Off" label at zero). Cosmetic only —
|
||||
multiplies the time-bonus shown in the win modal but does NOT
|
||||
affect achievement unlock thresholds (those still use the raw
|
||||
unmultiplied score).
|
||||
- **Win-replay recording + storage.** Every move during a successful
|
||||
game appends to a `RecordingReplay` resource; on `GameWonEvent`
|
||||
the recording freezes into a `Replay` (seed + draw_mode + mode +
|
||||
score + time + ordered move list) and persists to
|
||||
`<data_dir>/latest_replay.json` atomically. Single-slot — overwrites
|
||||
on every win.
|
||||
- **"Watch replay" button** in the Stats overlay. Shows the latest
|
||||
win's caption and surfaces a button that loads the replay (button
|
||||
fires an `InfoToastEvent` describing the replay; full in-engine
|
||||
playback is deferred to a future build).
|
||||
- **Replay upload + fetch endpoints** on the server. `POST /api/replays`
|
||||
accepts a `Replay` JSON; `GET /api/replays/:id` returns it. JWT-gated
|
||||
with the existing auth middleware. Engine uploads winning replays
|
||||
automatically when the player has cloud sync configured.
|
||||
- **`solitaire_wasm` crate** — new workspace member compiling
|
||||
replay-relevant `solitaire_core` types to WebAssembly so a
|
||||
browser can re-execute a replay client-side. No-std-friendly
|
||||
surface; `wasm-bindgen` glue.
|
||||
- **Web replay viewer** served from the Solitaire server.
|
||||
`GET /replays/:id` returns HTML + CSS + the wasm bundle that
|
||||
fetches the replay JSON, rasterises a deal from the seed, and
|
||||
animates the recorded moves.
|
||||
- **Card flight animations on the web side** so the browser viewer
|
||||
reads as a real game replay rather than a static dump.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Multi-card lift validation.** `solitaire_core::rules::is_valid_tableau_sequence`
|
||||
rejects a moved stack whose adjacent cards don't form a descending
|
||||
alternating-colour run. Previously a player could lift any
|
||||
multi-card selection and drop it as long as the bottom landed
|
||||
legally. Wired into `move_cards`'s tableau-destination branch.
|
||||
- **Softlock detection.** `has_legal_moves` rewritten to walk every
|
||||
potential move source (every stock card, every waste card, the
|
||||
face-up top of every tableau column) and check it against every
|
||||
foundation and every tableau. Previously the heuristic
|
||||
early-returned `true` whenever stock had cards — players got
|
||||
stuck in unwinnable end-states with no end-game screen.
|
||||
`GameOverScreen` now actually fires for true softlocks. Quat's
|
||||
exact reproduction case is pinned by a new test.
|
||||
- **Deal-tween information leak.** New-game now snaps every card
|
||||
sprite to the stock pile position before writing
|
||||
`StateChangedEvent`, so all 52 cards animate from a single point
|
||||
during the deal. Previously the sprites started from their
|
||||
previous-game positions, briefly revealing the prior deal.
|
||||
|
||||
### Documentation
|
||||
|
||||
- `SESSION_HANDOFF.md` refreshed for the Quat smoke-test round
|
||||
including investigation findings on solver decisions and
|
||||
dependency duplicates.
|
||||
|
||||
### Stats
|
||||
|
||||
- 1134 passing tests (was 1053 at v0.13.0 close).
|
||||
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||
|
||||
## [0.13.0] — 2026-05-02
|
||||
|
||||
Third UX iteration round on top of v0.12.0. Six handoff candidates
|
||||
shipped — three small polish items, three larger interaction
|
||||
features (theme-aware backs, full keyboard play, right-click power
|
||||
shortcut). Plus two code-review fixes (font handling unified,
|
||||
sccache wiring removed).
|
||||
|
||||
### Added
|
||||
|
||||
- **Tooltip-delay slider** in Settings → Gameplay. `tooltip_delay_secs`
|
||||
ranges [0.0, 1.5] in 0.1 s steps; "Instant" label when zero.
|
||||
`Settings.tooltip_delay_secs` round-trips through serialise/deserialise
|
||||
with `#[serde(default)]`. The hover-delay comparison in
|
||||
`ui_tooltip` reads from `SettingsResource` with the existing
|
||||
`MOTION_TOOLTIP_DELAY_SECS` as the test-fixture fallback.
|
||||
- **Win-streak fire animation.** New `WinStreakMilestoneEvent` fires
|
||||
from `stats_plugin` when `win_streak_current` crosses any of
|
||||
[3, 5, 10] (only the threshold crossing — not every subsequent
|
||||
win). The HUD streak readout scale-pulses 1.0 → 1.20 → 1.0 over
|
||||
`MOTION_STREAK_FLOURISH_SECS` (0.6 s).
|
||||
- **Score-breakdown reveal on the win modal.** Replaces the single
|
||||
"Score: N" line with a per-component reveal (Base / Time bonus /
|
||||
No-undo bonus / Mode multiplier / Total). Rows fade in over
|
||||
`MOTION_SCORE_BREAKDOWN_FADE_SECS` (0.12 s) staggered by
|
||||
`MOTION_SCORE_BREAKDOWN_STAGGER_SECS` (0.15 s). Honours
|
||||
`AnimSpeed::Instant` by spawning all rows fully visible.
|
||||
- **Card backs follow the active theme.** `theme.ron`'s `back` slot
|
||||
now actually drives the face-down sprite. Active-theme back
|
||||
rasterises alongside the faces and supersedes the legacy
|
||||
`back_N.png` picker. The picker remains as a fallback for themes
|
||||
that don't ship a back, and the Settings UI surfaces a caption
|
||||
("Active theme provides its own back") + dimmed swatches when
|
||||
the override is in effect.
|
||||
- **Keyboard-only drag-and-drop.** Tab cycles draggable card stacks,
|
||||
Enter "lifts" the focused stack, arrow keys (or Tab) cycle the
|
||||
legal-destination targets only, Enter confirms, Esc cancels. A
|
||||
new `KeyboardDragState` resource models the two-mode flow without
|
||||
changing the existing `SelectionState` contract. Mutual exclusion
|
||||
with mouse drag uses a sentinel `DragState.active_touch_id =
|
||||
KEYBOARD_DRAG_TOUCH_ID` (u64::MAX) so neither pipeline can
|
||||
trample the other.
|
||||
- **Right-click radial menu.** Hold right-click on a face-up card →
|
||||
a small ring of icons appears at the cursor with one entry per
|
||||
legal destination. Release over an icon → fires
|
||||
`MoveRequestEvent`; release in dead space, Esc, or left-click
|
||||
cancels. Skips the drag motion entirely. New `RadialMenuPlugin`
|
||||
owns the flow; co-exists with the existing `RightClickHighlight`
|
||||
pile-marker tint.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Font handling consolidated to bundled-only.** Code-review
|
||||
feedback: the SVG rasteriser previously mixed
|
||||
`load_system_fonts` + bundled FiraMono + a lenient resolver,
|
||||
which made card text rendering depend on host fontconfig. Picked
|
||||
option (a) and applied it across both layers — `font_plugin` now
|
||||
embeds `assets/fonts/main.ttf` via `include_bytes!()` and
|
||||
registers it with `Assets<Font>`; `svg_loader::shared_fontdb`
|
||||
loads only the bundled bytes; the new `bundled_font_resolver`
|
||||
ignores the SVG's `font-family` request and always returns the
|
||||
single bundled face. A parse failure aborts with a clear error
|
||||
("bundled FiraMono failed to parse — binary is corrupt").
|
||||
|
||||
### Removed
|
||||
|
||||
- **Project-level sccache wiring.** Code-review feedback: sccache
|
||||
shouldn't be a per-project build dependency. Cargo's incremental
|
||||
cache already covers the single-project case, and forcing
|
||||
`rustc-wrapper = "sccache"` workspace-wide meant every contributor
|
||||
had to install it. `.cargo/config.toml` deleted entirely; plain
|
||||
`cargo build` now works without setup.
|
||||
|
||||
### Documentation
|
||||
|
||||
- `help_plugin` controls reference gains a "Mouse" section covering
|
||||
double-click auto-move, right-click highlight, and the new
|
||||
hold-RMB radial.
|
||||
- `help_plugin` also gains a "Keyboard drag" section for the new
|
||||
Tab/Enter/Arrows/Esc flow.
|
||||
- Onboarding slide 3 picks up a `Tab → Enter` row referencing the
|
||||
full keyboard drag path.
|
||||
|
||||
### Stats
|
||||
|
||||
- 1053 passing tests (was 1031 at v0.12.0 close).
|
||||
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||
|
||||
## [0.12.0] — 2026-05-02
|
||||
|
||||
UX feel polish round on top of v0.11.0. Six small-but-tangible
|
||||
improvements that make the play surface feel more responsive,
|
||||
forgiving, and discoverable, plus the doc refresh that should have
|
||||
ridden along with v0.11.0.
|
||||
|
||||
### Added
|
||||
|
||||
- **Foundation completion flourish.** When a King lands on a
|
||||
foundation (Ace-through-King for that suit), a brief celebration
|
||||
fires: King card scale-pulses 1.0 → 1.15 → 1.0 over 0.4 s, the
|
||||
foundation marker tints `STATE_SUCCESS` for the first half then
|
||||
fades, and a synthesised C6→E6→G6 bell ping plays (~240 ms,
|
||||
octave above `win_fanfare`'s root so the fourth completion + win
|
||||
cascade layer cleanly). New `FoundationCompletedEvent { slot,
|
||||
suit }` carries the trigger so future systems can hook in.
|
||||
- **Drag-cancel return tween.** Illegal drops glide each dragged
|
||||
card back to its origin slot over 150 ms with a quintic ease-out
|
||||
curve (`MotionCurve::Responsive`, zero overshoot — reads forgiving
|
||||
rather than jittery). The audio cue (`card_invalid.wav`) still
|
||||
fires for negative feedback. Right-click and double-click invalid
|
||||
paths still use `ShakeAnim` since there's no motion to interpolate.
|
||||
- **Focus ring breathing.** The keyboard focus ring's alpha modulates
|
||||
with a 1.4 s sin curve over [0.65, 1.0] of its native value so the
|
||||
indicator catches the eye on focus changes without competing with
|
||||
gameplay. Honours `AnimSpeed::Instant` by reverting to the static
|
||||
outline for reduced-motion users.
|
||||
- **First-win achievement onboarding toast.** After the player's
|
||||
very first win, a one-shot info toast surfaces "First win! Press
|
||||
A to see your achievements." `Settings.shown_achievement_onboarding`
|
||||
persists the seen state so the cue never re-fires (legacy
|
||||
`settings.json` files load to `false` via `#[serde(default)]`).
|
||||
- **Mode Launcher digit shortcuts.** Pressing M opens the Home modal
|
||||
(the Mode Launcher); inside it, pressing 1–5 launches each mode
|
||||
directly without needing Tab + Enter. Locked modes (Zen, Challenge,
|
||||
Time Attack at level < 5) are silent no-ops. Modal-scoped — digit
|
||||
keys outside the launcher fire nothing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Card aspect ratio matches hayeah SVGs.** `CARD_ASPECT` 1.4 →
|
||||
1.4523 to match the bundled artwork's natural 167.087 × 242.667
|
||||
dimensions. Cards previously rendered ~3.6 % vertically squashed.
|
||||
The vertical-budget math in `compute_layout` uses `CARD_ASPECT`
|
||||
algebraically so the worst-case-tableau-fits-on-screen guarantee
|
||||
adapts automatically.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **README refresh** with v0.11.0+ features (card themes, HUD
|
||||
overhaul, drag feel, unlocked foundations) and a corrected controls
|
||||
table — the previous table inverted Z/U for undo and listed H for
|
||||
help when F1 is the binding.
|
||||
- **CHANGELOG.md** added (this file), covering v0.9.0–v0.12.0 with
|
||||
Keep a Changelog 1.1.0 conventions.
|
||||
|
||||
### Stats
|
||||
|
||||
- 1007 passing tests (was 982 at v0.11.0).
|
||||
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||
|
||||
## [0.11.0] — 2026-05-02
|
||||
|
||||
The biggest release since 0.10.0. Headline threads: a runtime card-theme
|
||||
system, an HUD restructure that reclaims the play surface, and a round of
|
||||
UX feel polish surfaced by smoke testing.
|
||||
|
||||
### Added
|
||||
|
||||
- **Runtime card-theme system** (CARD_PLAN phases 1–7).
|
||||
- Bundled default theme ships in the binary via `embedded://` — 52
|
||||
[hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets)
|
||||
SVGs (MIT) plus a midnight-purple `back.svg` as original work.
|
||||
- User themes live under `themes://` rooted at `user_theme_dir()`. Drop
|
||||
a directory containing `theme.ron` + 53 SVGs and the registry picks
|
||||
it up on next launch.
|
||||
- Importer at `solitaire_engine::theme::import_theme(zip)` validates
|
||||
archives (20 MB cap, zip-slip rejection, manifest validation, every
|
||||
SVG round-tripped through the rasteriser) and atomically unpacks.
|
||||
- Picker UI in **Settings → Cosmetic**; selection persists as
|
||||
`selected_theme_id` and propagates to live sprites.
|
||||
- **Reserved HUD top band** (64 px) so cards no longer crowd the score
|
||||
readout or action buttons; layout's `top_y` shifts down accordingly.
|
||||
- **Action-bar auto-fade** — buttons fade out when the cursor leaves the
|
||||
band, fade back in when it returns. Lerp at ~167 ms.
|
||||
- **Visible drop-target overlay during drag** — a soft fill plus 3 px
|
||||
outline drawn ABOVE stacked cards for every legal target (full fanned
|
||||
column for tableaux, card-sized for foundations and empty tableaux).
|
||||
Replaces the previously invisible pile-marker tint.
|
||||
- **Card drop shadows** — every card casts a neutral 25 % black shadow
|
||||
with a 4 px halo; cards in the active drag set switch to a lifted
|
||||
shadow (40 % alpha, larger offset, bigger halo).
|
||||
- **Stock remaining-count badge** — small `·N` chip at the top-right of
|
||||
the stock pile so the player can see how close they are to a recycle.
|
||||
Hides when the stock empties.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Foundations are unlocked.** `PileType::Foundation(Suit)` →
|
||||
`Foundation(u8)` (slot 0..3). The claimed suit is derived from the
|
||||
bottom card via `Pile::claimed_suit()` — no separate field, no
|
||||
claim-stuck-after-undo bugs. Any Ace lands in any empty slot, and the
|
||||
slot then claims that suit. `next_auto_complete_move` prefers a
|
||||
claim-matched slot before falling back to the first empty slot for
|
||||
Aces. Empty foundation markers render as plain placeholders (no
|
||||
"C/D/H/S").
|
||||
- **HUD selection label** and **hint toast** read `claimed_suit()` and
|
||||
fall through to "Foundation N" / "move to foundation" only when the
|
||||
slot is empty.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`shared_fontdb` now bundles FiraMono.** The hayeah SVGs reference
|
||||
`Bitstream Vera Sans` and `Arial` by name. On minimal Linux installs
|
||||
/ fresh Wayland sessions / chroots where neither is installed AND the
|
||||
CSS-generic aliases don't resolve, card rank/suit text vanished. The
|
||||
bundled font is loaded into fontdb and pinned as every CSS generic's
|
||||
target so the resolver always lands on something real. Surfaced when
|
||||
a second-machine pull rendered cards without glyphs.
|
||||
- **Theme asset path resolution** — `AssetPath::resolve` (concatenates)
|
||||
→ `resolve_embed` (RFC 1808 sibling resolution). Was producing paths
|
||||
like `…/theme.ron/hearts_4.svg` and failing to load every face SVG.
|
||||
- **Sync exit log spam** — `push_on_exit` silently no-ops on
|
||||
`LocalOnlyProvider`'s `UnsupportedPlatform` instead of warn-spamming
|
||||
every shutdown.
|
||||
- **usvg font-substitution warn spam** — custom `FontResolver.select_font`
|
||||
appends `Family::SansSerif` and `Family::Serif` to every query so
|
||||
unmatched named families silently fall through.
|
||||
|
||||
### Migration
|
||||
|
||||
- **In-progress saves invalidated.** `GameState.schema_version` bumped
|
||||
1 → 2; pre-v2 `game_state.json` files silently fall through to "fresh
|
||||
game on launch." Stats, progress, achievements, and settings live in
|
||||
separate files and are unaffected.
|
||||
|
||||
### Stats
|
||||
|
||||
- 982 passing tests (was 819 at v0.10.0).
|
||||
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||
|
||||
## [0.10.0] — 2026-04-29
|
||||
|
||||
PNG art pipeline plus a major dependency pass. The first release where
|
||||
the binary shipped with bundled artwork.
|
||||
|
||||
### Added
|
||||
|
||||
- **52 individual card face PNGs** generated via `solitaire_assetgen`.
|
||||
- **Custom font** (FiraMono-Medium) loaded via `AssetServer` at startup
|
||||
through the new `FontPlugin`.
|
||||
- **Card backs and backgrounds** upgraded to 120×168 with richer
|
||||
patterns.
|
||||
- **Ambient audio loop** wired through the kira mixer.
|
||||
- **Arch Linux PKGBUILDs** for the game client and sync server (under
|
||||
the separate `solitaire-quest-pkgbuild` directory).
|
||||
- **Workspace README, CI workflow, migration guide.**
|
||||
|
||||
### Changed
|
||||
|
||||
- **Bevy 0.15 → 0.18** workspace migration.
|
||||
- **kira 0.9 → 0.12** audio backend migration.
|
||||
- **Edition 2024**, MSRV pinned to **Rust 1.95**.
|
||||
- **rand 0.9** upgrade.
|
||||
- **Card rendering** moved from `Text2d` overlay to PNG-backed
|
||||
`Sprite` with face/back atlases; `Text2d` retained as a headless
|
||||
fallback when `CardImageSet` is absent (tests under MinimalPlugins).
|
||||
- **Asset pipeline** switched from `include_bytes!()` for PNGs/TTFs to
|
||||
runtime `AssetServer::load()` so artwork can be swapped without a
|
||||
recompile. Audio remains embedded.
|
||||
- **Removed Google Play Games Services sync backend** — redundant with
|
||||
the self-hosted server.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Server JWT secret** loaded at startup (was lazy, surfaced as
|
||||
intermittent 500s).
|
||||
- **Daily-challenge race** in the server's seed-generation path.
|
||||
- **Rate limiter** switched to `SmartIpKeyExtractor` so the limit
|
||||
applies per real client IP rather than per upstream proxy.
|
||||
- **Touch input** uses `MessageReader<TouchInput>` (Bevy 0.18 rename).
|
||||
- **Sync push/pull races** in async task scheduling.
|
||||
- **Hot-path allocations** reduced in card-rendering systems.
|
||||
- **Conflict report coverage** added for sync merge edge cases.
|
||||
|
||||
### Stats
|
||||
|
||||
- 819 passing tests at tag time.
|
||||
|
||||
## [0.9.0] — 2026-04-28
|
||||
|
||||
Initial public-tagged release. Established the workspace structure
|
||||
(`solitaire_core` / `_sync` / `_data` / `_engine` / `_server` / `_app` /
|
||||
`_assetgen`), the modal scaffold via `ui_modal`, the design-token system
|
||||
in `ui_theme`, and the four-tier HUD layout. Foundations were
|
||||
suit-locked at this point; cards rendered as `Text2d` rank/suit overlays
|
||||
with no PNG artwork yet.
|
||||
|
||||
### Added
|
||||
|
||||
- Klondike core (Draw One / Draw Three modes).
|
||||
- Progression system (XP, levels, 18 achievements, daily challenge,
|
||||
weekly goals, special modes at level 5).
|
||||
- Self-hosted sync server (Axum + SQLite + JWT auth).
|
||||
- All 12 overlay screens migrated to the `ui_modal` scaffold with real
|
||||
Primary/Secondary/Tertiary buttons.
|
||||
- Animation upgrades: `SmoothSnap` slide curves, scoped settle bounce,
|
||||
deal jitter, win-cascade rotation.
|
||||
- Splash screen, focus rings (Phases 1–3), tooltips infrastructure +
|
||||
HUD/Settings/popover applications, achievement integration tests,
|
||||
destructive-confirm verb unification, leaderboard error/idle states,
|
||||
first-launch empty-state polish, hit-target accessibility fix,
|
||||
CREDITS.md, persistent window geometry, mode-launcher Home repurpose,
|
||||
client-side sync round-trip integration tests.
|
||||
|
||||
[Unreleased]: https://github.com/funman300/Rusty_Solitaire/compare/v0.14.0...HEAD
|
||||
[0.14.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.13.0...v0.14.0
|
||||
[0.13.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.12.0...v0.13.0
|
||||
[0.12.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.11.0...v0.12.0
|
||||
[0.11.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.10.0...v0.11.0
|
||||
[0.10.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.9.0...v0.10.0
|
||||
[0.9.0]: https://github.com/funman300/Rusty_Solitaire/releases/tag/v0.9.0
|
||||
@@ -12,7 +12,6 @@ solitaire_sync/ # Shared API types — NO Bevy, serde/uuid/chrono only
|
||||
solitaire_data/ # Persistence + SyncProvider trait + server client
|
||||
solitaire_engine/ # Bevy ECS systems, components, plugins
|
||||
solitaire_server/ # Axum sync server binary
|
||||
solitaire_gpgs/ # Google Play Games bridge — STUB ONLY until Android phase
|
||||
solitaire_app/ # Thin binary entry point
|
||||
assets/ # Source assets — embedded at compile time via include_bytes!()
|
||||
```
|
||||
@@ -48,12 +47,13 @@ cargo clippy -p solitaire_core -- -D warnings
|
||||
|
||||
- `solitaire_core` and `solitaire_sync` must never gain Bevy or network dependencies.
|
||||
- No `unwrap()` or `panic!()` in game logic. All state transitions return `Result<_, MoveError>`.
|
||||
- Assets are embedded at compile time using `include_bytes!()`. No runtime asset loading via `AssetServer`.
|
||||
- Audio assets are embedded at compile time using `include_bytes!()` in `audio_plugin.rs`.
|
||||
- Card faces (52 PNGs in `assets/cards/faces/`), card backs (`assets/cards/backs/back_N.png`), board backgrounds (`assets/backgrounds/bg_N.png`), and the UI font (`assets/fonts/main.ttf`) are loaded at runtime via `AssetServer::load()` and stored as `Handle<Image>`/`Handle<Font>` in the `CardImageSet`, `BackgroundImageSet`, and `FontResource` resources. The `assets/` directory must ship alongside the binary.
|
||||
- Asset-loading systems take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` (tests). When `CardImageSet` is absent, `card_plugin` falls back to a `Text2d` rank+suit overlay; when `BackgroundImageSet` is absent, the board falls back to a solid colour.
|
||||
- Atomic file writes only: write to `filename.json.tmp`, then `rename()`.
|
||||
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
|
||||
- Sync runs on `AsyncComputeTaskPool` — never block the Bevy main thread.
|
||||
- All sync backends implement the `SyncProvider` trait. The `SyncPlugin` is backend-agnostic — never `match` on `SyncBackend` inside a Bevy system.
|
||||
- `solitaire_gpgs` is a stub until Android work begins. Do not write JNI bindings yet; keep the compile-time stub so the trait contract is enforced from day one.
|
||||
- `cargo clippy --workspace -- -D warnings` must pass clean after every change.
|
||||
- `cargo test --workspace` must pass after every change.
|
||||
|
||||
@@ -77,6 +77,7 @@ cargo clippy -p solitaire_core -- -D warnings
|
||||
- Resources own shared state. Events communicate between systems. Components own per-entity data.
|
||||
- All UI screens are built with Bevy UI (`bevy::ui`). Never mix UI layout and game logic in the same system.
|
||||
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
|
||||
- **UI-first.** Every player-triggered action (new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, switch mode, etc.) must be reachable from a visible UI control. Keyboard shortcuts are optional accelerators — never the sole entry point. New gameplay features ship with the UI control alongside the system that backs it; do not merge a feature that is keyboard-only.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Credits
|
||||
|
||||
Solitaire Quest is MIT-licensed (see [LICENSE](LICENSE)). It is built on top of
|
||||
the work of many open-source projects and a small handful of third-party
|
||||
assets. This file lists every component that ships in the binary or in the
|
||||
`assets/` directory.
|
||||
|
||||
---
|
||||
|
||||
## Code & Framework
|
||||
|
||||
| Component | License | Role |
|
||||
|---|---|---|
|
||||
| [Bevy 0.18](https://bevyengine.org/) | MIT OR Apache-2.0 | Game engine, ECS, rendering, UI |
|
||||
| [kira 0.12](https://crates.io/crates/kira) | MIT OR Apache-2.0 | Audio playback (mixer, sub-tracks, looping ambient) |
|
||||
| [serde](https://crates.io/crates/serde) / [serde_json](https://crates.io/crates/serde_json) | MIT OR Apache-2.0 | Serialization for save files and the sync API |
|
||||
| [tokio](https://crates.io/crates/tokio) | MIT | Async runtime for the sync client and server |
|
||||
| [axum 0.8](https://crates.io/crates/axum) | MIT | HTTP framework for the self-hosted sync server |
|
||||
| [sqlx 0.8](https://crates.io/crates/sqlx) | MIT OR Apache-2.0 | Compile-time-checked SQLite access on the server |
|
||||
| [reqwest 0.13](https://crates.io/crates/reqwest) | MIT OR Apache-2.0 | HTTP client for the sync provider |
|
||||
| [jsonwebtoken 10](https://crates.io/crates/jsonwebtoken) | MIT | JWT issuance and validation |
|
||||
| [bcrypt 0.19](https://crates.io/crates/bcrypt) | MIT | Password hashing on the server |
|
||||
| [keyring 4](https://crates.io/crates/keyring) | MIT OR Apache-2.0 | OS keychain integration for credential storage |
|
||||
| [tower-governor 0.8](https://crates.io/crates/tower-governor) | MIT | Rate limiting on `/api/auth/*` |
|
||||
| [chrono](https://crates.io/crates/chrono) | MIT OR Apache-2.0 | Date / time handling |
|
||||
| [uuid](https://crates.io/crates/uuid) | MIT OR Apache-2.0 | User and session identifiers |
|
||||
| [thiserror](https://crates.io/crates/thiserror) | MIT OR Apache-2.0 | Error type derive |
|
||||
| [rand 0.9](https://crates.io/crates/rand) | MIT OR Apache-2.0 | Seeded shuffler in `solitaire_core` |
|
||||
| [png 0.17](https://crates.io/crates/png) | MIT OR Apache-2.0 | PNG encoder used by `solitaire_assetgen` |
|
||||
| [ab_glyph 0.2](https://crates.io/crates/ab_glyph) | Apache-2.0 | Glyph rasterization for generated card art |
|
||||
|
||||
The full transitive dependency tree (several hundred crates) is captured in
|
||||
`Cargo.lock` and reachable via `cargo tree`. Every crate brought in is
|
||||
MIT, Apache-2.0, BSD-style, or a dual-licensed combination thereof — no
|
||||
copyleft code is statically linked into the game binary.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
### Card artwork
|
||||
|
||||
| File(s) | Source | License |
|
||||
|---|---|---|
|
||||
| `solitaire_engine/assets/themes/default/{suit}_{rank}.svg` (52 SVGs) | [hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets) | MIT |
|
||||
| `solitaire_engine/assets/themes/default/back.svg` | Original — Solitaire Quest | MIT (this project) |
|
||||
| `assets/cards/faces/{RANK}{SUIT}.png` (52 PNGs) | Pre-rendered from the same `playing-cards-assets` SVGs | MIT (passed through from hayeah) |
|
||||
| `assets/cards/backs/back_0.png` – `back_4.png` | Original — generated by `solitaire_assetgen::gen_art` | MIT (this project) |
|
||||
|
||||
The face SVGs come from Howard Yeh's `playing-cards-assets` repository, which
|
||||
is itself derived from the public-domain `vector-playing-cards` Google Code
|
||||
project. The art is redistributed under the MIT license — see the upstream
|
||||
repository for the full notice. The files ship unmodified in the bundled
|
||||
default theme; user-supplied themes can override them per-installation
|
||||
through the runtime SVG theming system documented in `CARD_PLAN.md`.
|
||||
|
||||
The default card back is original work by this project, midnight-purple
|
||||
themed to match the rest of the UI palette.
|
||||
|
||||
### Backgrounds
|
||||
|
||||
| File(s) | Source | License |
|
||||
|---|---|---|
|
||||
| `assets/backgrounds/bg_0.png` – `bg_4.png` | Original — generated by `solitaire_assetgen::gen_art` | MIT (this project) |
|
||||
|
||||
### Typography
|
||||
|
||||
| File | Source | License |
|
||||
|---|---|---|
|
||||
| `assets/fonts/main.ttf` (FiraMono-Medium) | [mozilla/Fira](https://github.com/mozilla/Fira) | SIL Open Font License 1.1 |
|
||||
|
||||
The OFL permits redistribution and embedding in software so long as the font
|
||||
file itself is not sold standalone. The file ships unmodified.
|
||||
|
||||
### Audio
|
||||
|
||||
All six WAV files in `assets/audio/` are **original work** — there are no
|
||||
third-party audio samples in this project. They are synthesized
|
||||
programmatically by `solitaire_assetgen/src/bin/gen_sfx.rs`, which writes
|
||||
44.1 kHz mono 16-bit PCM WAVs using a hand-rolled WAV writer (no `hound` or
|
||||
`dasp` dependency). The synthesis stack is entirely additive: sine /
|
||||
square waves, layered harmonics, deterministic LCG noise, AR envelopes,
|
||||
and a slow LFO for the ambient track.
|
||||
|
||||
| File | Synthesis approach |
|
||||
|---|---|
|
||||
| `card_deal.wav` | Filtered LCG noise with a sweeping low-pass cutoff for a "whoosh" |
|
||||
| `card_flip.wav` | High-passed LCG noise under a fast AR envelope |
|
||||
| `card_place.wav` | 120 Hz sine body + filtered noise click |
|
||||
| `card_invalid.wav` | Two dissonant square tones (196 Hz + 207.65 Hz) beating against each other |
|
||||
| `win_fanfare.wav` | C-major arpeggio (C5 / E5 / G5 / C6) with sine + 2nd harmonic |
|
||||
| `ambient_loop.wav` | 55 Hz fundamental with 2nd and 3rd harmonics, modulated by a 0.2 Hz LFO; loop length is chosen so the tone and LFO both complete an integer number of cycles for seamless looping |
|
||||
|
||||
Audio files are MIT-licensed alongside the rest of this project.
|
||||
|
||||
---
|
||||
|
||||
## License Summary
|
||||
|
||||
- **Project code:** MIT — see [LICENSE](LICENSE).
|
||||
- **Card face artwork (52 SVGs from hayeah/playing-cards-assets, plus the
|
||||
pre-rendered PNGs in `assets/cards/faces/`):** MIT, redistributed
|
||||
unmodified. The original `vector-playing-cards` line art is itself
|
||||
public domain.
|
||||
- **FiraMono-Medium font:** SIL Open Font License 1.1, redistributed unmodified.
|
||||
- **All other assets** (backgrounds, the default `back.svg`, generated card
|
||||
backs, every audio file) are original work covered by this project's MIT
|
||||
license.
|
||||
|
||||
If you redistribute Solitaire Quest, you must ship this `CREDITS.md` and the
|
||||
`LICENSE` file alongside the binary so the MIT (project + hayeah card art)
|
||||
and OFL (FiraMono) notices remain visible to end users.
|
||||
@@ -0,0 +1,3 @@
|
||||
{$SOLITAIRE_DOMAIN} {
|
||||
reverse_proxy server:{$SERVER_PORT:-8080}
|
||||
}
|
||||
@@ -5,41 +5,69 @@ members = [
|
||||
"solitaire_data",
|
||||
"solitaire_engine",
|
||||
"solitaire_server",
|
||||
"solitaire_gpgs",
|
||||
"solitaire_app",
|
||||
"solitaire_assetgen",
|
||||
"solitaire_wasm",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
version = "0.1.0"
|
||||
license = "MIT"
|
||||
rust-version = "1.95"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
thiserror = "2"
|
||||
rand = "0.9"
|
||||
async-trait = "0.1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
dirs = "5"
|
||||
keyring = "2"
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||
dirs = "6"
|
||||
keyring = "4"
|
||||
keyring-core = "1"
|
||||
reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
|
||||
|
||||
solitaire_core = { path = "solitaire_core" }
|
||||
solitaire_sync = { path = "solitaire_sync" }
|
||||
solitaire_data = { path = "solitaire_data" }
|
||||
solitaire_engine = { path = "solitaire_engine" }
|
||||
|
||||
bevy = "0.15"
|
||||
kira = "0.9"
|
||||
bevy = "0.18"
|
||||
kira = "0.12"
|
||||
|
||||
axum = "0.7"
|
||||
# SVG rasterisation pipeline for the runtime card-theme system.
|
||||
# usvg parses + simplifies; resvg renders to a tiny-skia Pixmap;
|
||||
# tiny-skia provides the CPU rasteriser. All three are maintained
|
||||
# together by the resvg-rs project and version in lockstep.
|
||||
usvg = "0.47"
|
||||
resvg = "0.47"
|
||||
tiny-skia = "0.12"
|
||||
|
||||
# Theme manifest format. RON keeps the file human-editable while
|
||||
# preserving Rust-style structures the importer can validate.
|
||||
ron = "0.12"
|
||||
|
||||
# Importer-only: reads user-supplied theme zip archives, validates
|
||||
# their contents, and unpacks them into the user themes directory.
|
||||
# Default features are disabled to keep the dependency footprint small;
|
||||
# only `deflate` is needed because the importer rejects other
|
||||
# compression methods anyway (see Phase 7 spec).
|
||||
zip = { version = "8.6", default-features = false, features = ["deflate"] }
|
||||
|
||||
# Importer-only test dependency: tests build zip archives in a
|
||||
# scratch directory so they don't pollute the real user themes path
|
||||
# on the developer's machine.
|
||||
tempfile = "3.27"
|
||||
|
||||
axum = "0.8"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
jsonwebtoken = "9"
|
||||
bcrypt = "0.15"
|
||||
tower_governor = "0.4"
|
||||
jsonwebtoken = { version = "10", default-features = false, features = ["rust_crypto"] }
|
||||
bcrypt = "0.19"
|
||||
tower_governor = "0.8"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
dotenvy = "0.15"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Stage 1 — builder
|
||||
# Compiles the solitaire_server binary in release mode.
|
||||
# Requires a pre-generated .sqlx/ query cache (run `cargo sqlx prepare --workspace`
|
||||
# before building the image so sqlx macros work without a live database).
|
||||
FROM rust:slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
# Tell sqlx to use the cached query metadata instead of a live database.
|
||||
ENV SQLX_OFFLINE=true
|
||||
|
||||
RUN cargo build --release -p solitaire_server
|
||||
|
||||
# Stage 2 — runtime
|
||||
# Minimal image that only contains the compiled binary and its runtime deps.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/target/release/solitaire_server /usr/local/bin/solitaire_server
|
||||
|
||||
EXPOSE ${SERVER_PORT:-8080}
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/solitaire_server"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 funman300
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Solitaire Quest
|
||||
|
||||
A cross-platform Klondike Solitaire game written in Rust, with a card-theme
|
||||
system, full progression (XP / levels / achievements / daily challenges), and
|
||||
optional self-hosted sync so your stats follow you across machines.
|
||||
|
||||
## Features
|
||||
|
||||
- **Klondike Solitaire** — Draw One and Draw Three modes; foundations are
|
||||
unlocked (any Ace lands in any empty slot, the slot then claims that suit)
|
||||
- **Card themes** — bundled hayeah/playing-cards-assets default plus
|
||||
user-installable themes (drop a directory under the data dir or import a
|
||||
zip from Settings → Cosmetic)
|
||||
- **Modern HUD** — reserved top band keeps cards from crowding the score
|
||||
readout; the action bar auto-fades when the cursor leaves it so it can't
|
||||
compete with the play surface
|
||||
- **Drag feel** — every legal drop target is highlighted in green during
|
||||
drag; cards cast a soft drop shadow that lifts when picked up; the stock
|
||||
pile shows a remaining-count chip so you can see how close you are to a
|
||||
recycle
|
||||
- **Keyboard navigation** — Tab cycles focus through buttons, arrow keys
|
||||
move within picker rows, Enter activates; works across every modal and
|
||||
the HUD action bar
|
||||
- **Progression** — XP, levels, unlockable card backs and backgrounds
|
||||
- **18 Achievements** — including secret ones
|
||||
- **Daily Challenge** — server-seeded so every player worldwide gets the
|
||||
same deal
|
||||
- **Leaderboard** — opt-in, powered by your own self-hosted server
|
||||
- **Special Modes** (unlocked at level 5): Zen, Time Attack, Challenge
|
||||
- **Sync** — pull/push stats across devices via a self-hosted server
|
||||
- **Color-blind mode** — blue tint on red-suit cards alongside the suit
|
||||
glyph
|
||||
|
||||
## Building
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
- Rust stable toolchain (`rustup install stable`)
|
||||
- Linux: `libasound2-dev libudev-dev libxkbcommon-dev`
|
||||
- macOS: Xcode Command Line Tools
|
||||
|
||||
```bash
|
||||
# Fast development build
|
||||
cargo run -p solitaire_app --features bevy/dynamic_linking
|
||||
|
||||
# Release build
|
||||
cargo build -p solitaire_app --release
|
||||
./target/release/solitaire_app
|
||||
```
|
||||
|
||||
## Controls
|
||||
|
||||
Every action also has a visible UI button — keyboard shortcuts are optional
|
||||
accelerators.
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| Left click / drag | Move cards |
|
||||
| Double click | Auto-move card to its best legal destination |
|
||||
| Right click | Highlight legal moves for a card |
|
||||
| Space / D | Draw from stock |
|
||||
| U | Undo |
|
||||
| H | Hint (highlight a legal move) |
|
||||
| N | New game |
|
||||
| Z | Zen mode |
|
||||
| G | Forfeit (during pause) |
|
||||
| Tab / Shift+Tab | Cycle keyboard focus |
|
||||
| Enter | Activate focused button / auto-complete (when badge is lit) |
|
||||
| Esc | Pause / dismiss modal |
|
||||
| F1 | Help / controls |
|
||||
| F11 | Toggle fullscreen |
|
||||
| S / A / P / O / L / M | Stats / Achievements / Profile / Settings / Leaderboard / Menu |
|
||||
|
||||
## Card themes
|
||||
|
||||
The default theme ships embedded in the binary, so the game runs
|
||||
self-contained with no external assets. To install another theme, drop a
|
||||
directory containing a `theme.ron` manifest plus 53 SVG files (52 faces +
|
||||
1 back) under the platform data dir's `themes/` folder, or import a zip
|
||||
from **Settings → Cosmetic**. The picker chip lights up the moment a new
|
||||
theme is registered. Themes are SVG-based, so they rasterise cleanly at
|
||||
whatever resolution the window happens to be.
|
||||
|
||||
## Sync Server (optional)
|
||||
|
||||
To sync stats across machines, run the self-hosted server. See
|
||||
[README_SERVER.md](README_SERVER.md) for setup instructions.
|
||||
|
||||
Once the server is running, open **Settings → Sync Backend**, enter the
|
||||
server URL and your username, and register an account from within the
|
||||
game.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# All tests (982 passing as of v0.11.0)
|
||||
cargo test --workspace
|
||||
|
||||
# Just game logic (no display required)
|
||||
cargo test -p solitaire_core -p solitaire_sync -p solitaire_data -p solitaire_server
|
||||
|
||||
# Lint
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Built on [Bevy](https://bevyengine.org/) and the wider Rust ecosystem
|
||||
(Tokio, Axum, sqlx, Serde, kira, and many more). Card faces come from
|
||||
[hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets)
|
||||
(MIT, derived from the public-domain `vector-playing-cards` library); the
|
||||
default card back is original work; the UI font is FiraMono-Medium (OFL).
|
||||
All audio is synthesized programmatically by this project. See
|
||||
[CREDITS.md](CREDITS.md) for the full list and license details.
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,44 @@
|
||||
# Solitaire Quest — Self-Hosting Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose
|
||||
- `openssl` for generating a JWT secret
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Clone the repo and enter it.
|
||||
2. Copy the example environment file and fill in your values:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env: set JWT_SECRET and SOLITAIRE_DOMAIN
|
||||
```
|
||||
3. (First time only) Generate the sqlx query cache so the server builds without a live database:
|
||||
```bash
|
||||
cargo install sqlx-cli --no-default-features --features rustls,sqlite
|
||||
export DATABASE_URL=sqlite://solitaire.db
|
||||
sqlx database create
|
||||
sqlx migrate run --source solitaire_server/migrations
|
||||
cargo sqlx prepare --workspace
|
||||
rm solitaire.db # the real DB lives in ./data/ at runtime
|
||||
```
|
||||
4. Start everything:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
5. The server is now reachable at `https://<SOLITAIRE_DOMAIN>`.
|
||||
|
||||
## Backups
|
||||
|
||||
The entire server state is one SQLite file at `./data/solitaire.db`. Back it up with:
|
||||
```bash
|
||||
sqlite3 ./data/solitaire.db ".backup backup_$(date +%Y%m%d).db"
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
@@ -0,0 +1,140 @@
|
||||
# Solitaire Quest — Session Handoff
|
||||
|
||||
**Last updated:** 2026-05-02 (session 9, post-v0.14.0 release prep) — v0.14.0 cut. The Quat bug fixes, the rest of the v0.13.0 candidate list, and the entire replay → upload → web-viewer pipeline are all bundled in this release. Direction now opens for the next round.
|
||||
|
||||
## Status at pause
|
||||
|
||||
- **HEAD on origin:** v0.14.0's tag commit (CHANGELOG + handoff refresh).
|
||||
- **Working tree:** clean apart from untracked `CARD_PLAN.md` (intentional).
|
||||
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings` clean.
|
||||
- **Tests:** **1134 passed / 0 failed** across the workspace.
|
||||
- **Tags on origin:** `v0.9.0`, `v0.10.0`, `v0.11.0`, `v0.12.0`, `v0.13.0`, `v0.14.0`.
|
||||
|
||||
## Where we are
|
||||
|
||||
v0.14.0 is the largest release since the card-theme system. Three threads land together:
|
||||
|
||||
1. **The remaining v0.13.0-era UX candidates** — theme thumbnails, daily-challenge calendar, Time Attack auto-save, per-mode bests, time-bonus multiplier slider.
|
||||
2. **Quat smoke-test bug fixes** — multi-card move validation, softlock detection, deal-tween information leak.
|
||||
3. **The replay pipeline** — record on win, persist to disk, upload to server, view in browser via a new `solitaire_wasm` crate. The biggest single feature since the card-theme system.
|
||||
|
||||
The card-flight web animations and replay E2E test coverage close out the pipeline.
|
||||
|
||||
### Design direction (unchanged)
|
||||
|
||||
- **Tone:** Balatro — chunky readable type, theatrical hierarchy, satisfying micro-interactions.
|
||||
- **Palette:** Midnight Purple base + Balatro yellow primary + warm magenta secondary.
|
||||
- See `~/.claude/projects/-home-manage-Rusty-Solitare/memory/project_ux_overhaul_2026-04.md` (machine-local).
|
||||
|
||||
### Canonical remote
|
||||
|
||||
`github.com/funman300/Rusty_Solitaire` is the canonical repo. Always push there.
|
||||
|
||||
## Session 8 + 9 (shipped 2026-05-02) — v0.14.0
|
||||
|
||||
### v0.13.0-era UX candidates (had landed but missed v0.13.0's tag)
|
||||
|
||||
| Area | Commit | What landed |
|
||||
|---|---|---|
|
||||
| Theme thumbnails | `ba527de` | Each Settings → Cosmetic theme chip renders an Ace + back preview pair via `rasterize_svg`. Cached per theme. Missing-SVG themes show a transparent placeholder rather than crashing. |
|
||||
| Daily-challenge calendar | `1a10476` | 14-dot horizontal calendar in the Profile modal. Today is ringed, completed days fill `STATE_SUCCESS`, missed days fill `BG_ELEVATED`. Caption: "Current streak: N · Longest: M". `PlayerProgress` gains `daily_challenge_history` (capped at 365) and `daily_challenge_longest_streak`. |
|
||||
| Time Attack auto-save | `0001432` | New sibling `time_attack_session.json` next to `game_state.json`. Atomic .tmp + rename. 30 s auto-save while active + on `AppExit`. Sessions whose 10-min window expired in real time while the app was closed are discarded on load. |
|
||||
| Per-mode bests | `3984231` | StatsSnapshot gains six `#[serde(default)]` fields (Classic / Zen / Challenge × best_score + fastest_win_seconds). Stats screen renders a "Per-mode bests" section. Lifetime totals continue to roll all modes together. |
|
||||
| Time-bonus slider | `89c51ab` | Settings → Gameplay slider 0.0–2.0, default 1.0, "Off" at zero. Multiplies the time-bonus shown in the win modal. Cosmetic only — does NOT affect achievement unlock thresholds. |
|
||||
|
||||
### Quat smoke-test bug fixes
|
||||
|
||||
| Area | Commit | What landed |
|
||||
|---|---|---|
|
||||
| Move validation (#1) | `f1aeb24` | `solitaire_core::rules::is_valid_tableau_sequence(&[Card]) -> bool` checks every adjacent pair in a moved stack descends one rank with alternating colour. Wired into `move_cards`. Closes the bug where any multi-card lift could be dropped as long as the bottom landed legally. |
|
||||
| Deal-tween leak (#4) | `3eabc14` | New-game snaps every card sprite to the stock pile position before writing `StateChangedEvent`, so all 52 cards animate from a single deck point during the deal. Previously sprites started from previous-game positions, briefly revealing the prior deal. |
|
||||
| Softlock detection (#2) | `2716472` | `has_legal_moves` rewritten: walks every potential move source (every stock card, every waste card, the face-up top of every tableau column) against every foundation and every tableau. Previous heuristic returned `true` whenever stock had cards, hiding genuine softlocks. `GameOverScreen` now actually fires for true softlocks. |
|
||||
| End-game screen (#3) | — | Resolved as downstream of #2. The pre-existing `GameOverScreen` and `WinSummaryOverlay` already cover the close-out paths; the softlock screen just never spawned because the old `has_legal_moves` lied. |
|
||||
|
||||
### Replay pipeline (the major feature)
|
||||
|
||||
| Area | Commit | What landed |
|
||||
|---|---|---|
|
||||
| Replay storage | `42535f5` | `solitaire_data::replay::Replay` (seed + draw_mode + mode + score + time + recorded date + ordered move list) and atomic save/load helpers under `<data_dir>/latest_replay.json`. Schema v1; `load` returns None for any other version. |
|
||||
| Engine recording | `57d1c58` | `RecordingReplay` resource + `ReplayPath` settings. Every successful `MoveRequestEvent` / `DrawRequestEvent` appends to recording; `GameWonEvent` freezes the recording into a `Replay` and persists. Undo intentionally not recorded. New game clears the recording. |
|
||||
| Stats button | `d9f36bf` | Stats overlay surfaces a "Latest win:" caption + "Watch replay" button. Loads from disk via `LatestReplayResource`. (Full in-engine playback deferred — button currently fires an `InfoToastEvent` describing the replay.) |
|
||||
| Server upload + fetch | `93182fa` | `POST /api/replays` accepts a `Replay` JSON; `GET /api/replays/:id` returns it. JWT-gated. SQL migration for the new `replays` table. |
|
||||
| Engine sync | `23c9704` | Engine uploads winning replays automatically when the player has cloud sync configured. Re-uses the existing JWT/refresh-token flow. |
|
||||
| WASM crate | `5bed43e` | New workspace member `solitaire_wasm` compiles replay-relevant `solitaire_core` types to WebAssembly so a browser can re-execute a replay client-side. `wasm-bindgen` glue. |
|
||||
| Web viewer | `07b8ecd` | `GET /replays/:id` returns HTML + CSS + the wasm bundle. Browser fetches the replay JSON, rasterises a deal from the seed, and animates the recorded moves. |
|
||||
| E2E coverage | `3081505` | Server tests covering the full upload → fetch round-trip via `axum::test`. |
|
||||
| Web flight anim | `1fcd032` | Card-flight tweens on the web side so the browser viewer reads as a real game replay rather than a static dump. |
|
||||
|
||||
## Open punch list
|
||||
|
||||
### Release prep
|
||||
1. **Smoke-test on the alex machine** after pulling — confirm Quat's three bug fixes hold up in real gameplay, and try the new replay button + web viewer end-to-end.
|
||||
2. **Desktop packaging** per `ARCHITECTURE.md §17`. The Arch PKGBUILD exists in `/home/manage/solitaire-quest-pkgbuild/` (separate repo). Pending: app icon, macOS `.icns` + notarisation cert, Windows `.ico` + Authenticode cert, AppImage recipe.
|
||||
|
||||
### UX iteration (next-round candidates)
|
||||
|
||||
- **Solver-at-deal toggle** (Quat investigation #1, still deferred): add a Settings → Gameplay toggle "Winnable deals only" rather than baking solver-only into every deal. Lightest middle ground.
|
||||
- **Disable Bevy's default audio feature** (Quat investigation #2, still deferred): one-line `default-features = false` swap on the workspace `bevy =` line, re-enable explicitly the features the engine uses (`render`, `bevy_winit`, `2d`, `bevy_window`, `png`, `bevy_text`, `bevy_ui`, `bevy_log`, `bevy_asset`, `default_font`, `bevy_state`). Drops ~50 transitive crates including the rodio + symphonia stack the project doesn't use (kira handles audio).
|
||||
- **In-engine replay playback** — promote the "Watch replay" button from a stub toast to a real playback overlay that re-runs the recorded moves with `CardAnimation` tweens. The wasm crate already proves the playback math; the in-engine version reuses the same execute logic against the live game state.
|
||||
- **Per-replay history** — currently single-slot at `latest_replay.json`. A "best replay per mode" bucket or a recent-N rolling list would let players revisit notable wins.
|
||||
- **Solver-driven hint system** — extend the existing hint toggle so a deal-time solver provides higher-quality hints (currently a heuristic). Requires the solver from the toggle work above.
|
||||
- **Achievement: "won via replay path"** — track when a player wins a deal whose previously-saved replay also won the same deal. Mostly fun; trivial scope.
|
||||
|
||||
## Card-theme system (CARD_PLAN.md, fully shipped)
|
||||
|
||||
Seven phases landed across `b8fb3fb` → `924a1e2` in v0.11.0; v0.13.0's `7ed4f2c` consumes the per-theme `back.svg`; v0.14.0's `ba527de` adds preview thumbnails. End-to-end:
|
||||
|
||||
- **Bundled default theme** ships in the binary via `embedded://` — 52 hayeah/playing-cards-assets SVGs + a midnight-purple `back.svg`.
|
||||
- **User themes** under `themes://`. Drop a directory containing `theme.ron` + 53 SVGs.
|
||||
- **Importer** at `solitaire_engine::theme::import_theme(zip)` validates archives and atomically unpacks.
|
||||
- **Picker UI** in Settings → Cosmetic; thumbnails + the active theme's `back` override the legacy `back_N.png` picker when present.
|
||||
|
||||
## Resume prompt
|
||||
|
||||
```
|
||||
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||
Working directory: <Rusty_Solitaire clone path on this machine — local
|
||||
directory may still be named Rusty_Solitare from earlier; that's fine>.
|
||||
Branch: master. Direction is OPEN — v0.14.0 just shipped covering the
|
||||
Quat bug fixes, the v0.13.0 candidate tail, and the entire
|
||||
replay-pipeline feature.
|
||||
|
||||
State: HEAD at v0.14.0. Working tree clean apart from untracked
|
||||
CARD_PLAN.md (intentional).
|
||||
Build: cargo clippy --workspace --all-targets -- -D warnings clean.
|
||||
Tests: 1134 passed / 0 failed.
|
||||
|
||||
READ FIRST (in order, before doing anything):
|
||||
1. SESSION_HANDOFF.md — v0.14.0 changelog + open punch list
|
||||
2. CHANGELOG.md — release-by-release record
|
||||
3. CLAUDE.md — hard rules (UI-first, no panics, etc.)
|
||||
4. ARCHITECTURE.md — crate responsibilities + data flow
|
||||
5. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
||||
— saved feedback / project context (machine-local;
|
||||
may be missing on a fresh machine)
|
||||
|
||||
DECISION TO ASK THE PLAYER FIRST:
|
||||
A. Smoke-test v0.14.0 on the alex machine first to confirm the
|
||||
three Quat bug fixes hold up in real gameplay and the replay
|
||||
pipeline works end-to-end (record → upload → web viewer).
|
||||
B. Take the deferred Bevy-audio-feature trim (Quat investigation
|
||||
#2) — one-line workspace edit, ~50 fewer transitive crates.
|
||||
C. Take the deferred solver toggle (Quat investigation #1): add
|
||||
"Winnable deals only" Settings toggle. Larger.
|
||||
D. Promote the in-engine "Watch replay" button to real playback.
|
||||
E. Pick from the remaining "next-round candidates" in this doc.
|
||||
F. Take the deferred desktop-packaging item (needs artwork +
|
||||
signing certs from the user).
|
||||
|
||||
WORKFLOW NOTES:
|
||||
- Commits use:
|
||||
git -c user.name=funman300 -c user.email=root@vscode.infinity \
|
||||
commit -m "..."
|
||||
- When attributing playtester feedback in commits/docs, use "Quat"
|
||||
not "Rhys" (saved feedback memory).
|
||||
- Sub-agents stage + verify only; orchestrator commits.
|
||||
- Every commit must pass build / clippy / test before pushing.
|
||||
- Push to GitHub (origin) — that is the canonical remote.
|
||||
|
||||
OPEN AT THE START: ask which of A–F. Don't pick unilaterally.
|
||||
```
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 357 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 256 KiB |