Compare commits
340 Commits
13b428b81c
...
v0.21.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 04f9bf9be3 | |||
| a292a7ead0 | |||
| d109c32b75 | |||
| dd101b3d54 | |||
| af414b6aed | |||
| ae84dc1504 | |||
| 8719f77ec2 | |||
| a14200ac2f | |||
| e8bf9d79da | |||
| 48b28d29f8 | |||
| babe5cc9c8 | |||
| 3a4bb63a6f | |||
| 56233687b0 | |||
| 73ac67d76b | |||
| a27cf5a020 | |||
| 29136d815d | |||
| ef54cdeb65 | |||
| e080b49914 | |||
| 54005d5494 | |||
| 44f5972edd | |||
| 13ae16051d | |||
| a65e5b8c7b | |||
| 6204db8bb1 | |||
| c84d9f445c | |||
| cacb19c03f | |||
| 39b84965b6 | |||
| 41a009a693 | |||
| fa7f98ac52 | |||
| 9891ae4ba3 | |||
| cdcaddaabe | |||
| d752870007 | |||
| 1d1543e4bc | |||
| 651f4060e6 | |||
| a1376075bd | |||
| ceec4fc486 | |||
| 0d477ac9fd | |||
| 4b51e50203 | |||
| f2d2119db5 | |||
| 59424a370c | |||
| fb8b2ac684 | |||
| 690e1d2ad6 | |||
| 35516d31f6 | |||
| 9b065e5ac6 | |||
| e1b8766e15 | |||
| 67c150bd7b | |||
| aa2a021712 | |||
| 6037596cc0 | |||
| d7ffb16df5 | |||
| b57db017d3 | |||
| 0b3140ad6d | |||
| e41def8c89 | |||
| aad8bb9c83 | |||
| 55c235b55f | |||
| 21ec03b157 | |||
| 17e3112502 | |||
| de4751115f | |||
| 9ff48ace5b | |||
| 91b7605b9f | |||
| 42d90b199c | |||
| 3e11e9e79a | |||
| bfcd05fbb5 | |||
| c497c3193c | |||
| 9aa0dd23b1 | |||
| d065d49fe7 | |||
| c30b04ec72 | |||
| 40d6e0ab17 | |||
| 9fe650fa20 | |||
| b73d246b4c | |||
| ae40a1db7a | |||
| b7c3a4996f | |||
| d48b9489db | |||
| 08b006ff30 | |||
| 17e0737a10 | |||
| dd63261999 | |||
| 93660c2217 | |||
| 56e2e6f151 | |||
| cc635328be | |||
| a4bc063497 | |||
| 540869c851 | |||
| bdac754b26 | |||
| f863d85c35 | |||
| 3c7a0eb4fb | |||
| d489e7a31b | |||
| f2f30c8002 | |||
| a49a340a30 | |||
| 27cdf78ce0 | |||
| faa6c5efc4 | |||
| 487b99bbc9 | |||
| 53e3b816cf | |||
| 87275bf340 | |||
| 56647d7f0d | |||
| cbf2483028 | |||
| a54201e97b | |||
| 48e412177c | |||
| cd54ce1bb0 | |||
| 7a3032b74c | |||
| 89699a8a86 | |||
| 70165da103 | |||
| 8a5fa8751c | |||
| bf660df971 | |||
| 13a8a012ee | |||
| 02ababa65f | |||
| 9c36b49729 | |||
| 8e90574437 | |||
| 95fcdad5d2 | |||
| 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 |
@@ -1,4 +1,16 @@
|
|||||||
DATABASE_URL=sqlite://solitaire.db
|
# Copy to .env and fill in the values before running docker compose up.
|
||||||
JWT_SECRET=replace_with_64_char_hex_from_openssl_rand_hex_32
|
|
||||||
|
# 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
|
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
|
/target
|
||||||
|
/.sccache-cache
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
.env
|
.env
|
||||||
*.tmp
|
*.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
|
# Solitaire Quest — Architecture Document
|
||||||
|
|
||||||
> **Version:** 1.1
|
> **Version:** 1.1
|
||||||
> **Language:** Rust (Edition 2021)
|
> **Language:** Rust (Edition 2024)
|
||||||
> **Engine:** Bevy (latest stable)
|
> **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)
|
5. [Game Engine Architecture](#5-game-engine-architecture)
|
||||||
6. [Persistence & Sync Architecture](#6-persistence--sync-architecture)
|
6. [Persistence & Sync Architecture](#6-persistence--sync-architecture)
|
||||||
7. [Sync Server Architecture](#7-sync-server-architecture)
|
7. [Sync Server Architecture](#7-sync-server-architecture)
|
||||||
8. [Google Play Games Services (Android Future)](#8-google-play-games-services-android-future)
|
8. [Data Models](#8-data-models)
|
||||||
9. [Data Models](#9-data-models)
|
9. [API Reference](#9-api-reference)
|
||||||
10. [API Reference](#10-api-reference)
|
10. [Merge Strategy](#10-merge-strategy)
|
||||||
11. [Merge Strategy](#11-merge-strategy)
|
11. [Achievement System](#11-achievement-system)
|
||||||
12. [Achievement System](#12-achievement-system)
|
12. [Progression System](#12-progression-system)
|
||||||
13. [Progression System](#13-progression-system)
|
13. [Audio System](#13-audio-system)
|
||||||
14. [Audio System](#14-audio-system)
|
14. [Asset Pipeline](#14-asset-pipeline)
|
||||||
15. [Asset Pipeline](#15-asset-pipeline)
|
15. [Platform Targets](#15-platform-targets)
|
||||||
16. [Platform Targets](#16-platform-targets)
|
16. [Build & Development Guide](#16-build--development-guide)
|
||||||
17. [Build & Development Guide](#17-build--development-guide)
|
17. [Deployment Guide](#17-deployment-guide)
|
||||||
18. [Deployment Guide](#18-deployment-guide)
|
18. [Security Model](#18-security-model)
|
||||||
19. [Security Model](#19-security-model)
|
19. [Testing Strategy](#19-testing-strategy)
|
||||||
20. [Testing Strategy](#20-testing-strategy)
|
20. [Decision Log](#20-decision-log)
|
||||||
21. [Decision Log](#21-decision-log)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Project Overview
|
## 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.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Sync Backend by Platform
|
### Sync Backend by Platform
|
||||||
|
|
||||||
@@ -46,17 +43,15 @@ On Android (stretch goal), sync is enhanced with Google Play Games Services (GPG
|
|||||||
| macOS | Self-hosted server | Full feature set |
|
| macOS | Self-hosted server | Full feature set |
|
||||||
| Windows | Self-hosted server | Full feature set |
|
| Windows | Self-hosted server | Full feature set |
|
||||||
| Linux | 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
|
### Design Principles
|
||||||
|
|
||||||
- **Offline first.** The local file is always the source of truth. Sync is additive, never destructive.
|
- **Offline first.** The local file is always the source of truth. Sync is additive, never destructive.
|
||||||
- **Pure core.** All game logic lives in a dependency-free Rust crate with no Bevy, no network, and no I/O. This keeps it fully unit-testable and portable.
|
|
||||||
- **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.
|
- **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.
|
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
|
||||||
|
|
||||||
|
Pure-core, no-panics-in-game-logic, and UI-first-interaction constraints are enforced by CLAUDE.md §2.1, §2.3, and §3.3 respectively — those are the canonical statements; this file describes the design that motivates them.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Workspace Structure
|
## 2. Workspace Structure
|
||||||
@@ -72,26 +67,25 @@ solitaire_quest/
|
|||||||
├── Dockerfile # Multi-stage server build
|
├── Dockerfile # Multi-stage server build
|
||||||
├── docker-compose.yml # Server + Caddy reverse proxy
|
├── 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/
|
│ ├── cards/
|
||||||
│ │ ├── faces/ # Card face sprites (suit + rank)
|
│ │ ├── faces/{RANK}{SUIT}.png # 52 card faces — rendered from hayeah/playing-cards-assets SVGs (MIT)
|
||||||
│ │ └── backs/ # Card back designs (back_0.png … back_4.png)
|
│ │ └── backs/back_0.png – back_4.png # back_0 = generated default back; back_1–4 are generated patterns
|
||||||
│ ├── backgrounds/ # Table backgrounds (bg_0.png … bg_4.png)
|
│ ├── backgrounds/bg_0.png – bg_4.png # generated textures
|
||||||
│ ├── fonts/ # .ttf font files
|
│ ├── fonts/main.ttf # FiraMono-Medium (170K, OFL)
|
||||||
│ └── audio/
|
│ └── audio/
|
||||||
│ ├── card_deal.ogg
|
│ ├── card_deal.wav
|
||||||
│ ├── card_flip.ogg
|
│ ├── card_flip.wav
|
||||||
│ ├── card_place.ogg
|
│ ├── card_place.wav
|
||||||
│ ├── card_invalid.ogg
|
│ ├── card_invalid.wav
|
||||||
│ ├── win_fanfare.ogg
|
│ ├── win_fanfare.wav
|
||||||
│ └── ambient_loop.ogg
|
│ └── ambient_loop.wav
|
||||||
│
|
│
|
||||||
├── solitaire_core/ # Pure Rust game logic — zero external deps beyond rand/serde
|
├── solitaire_core/ # Pure Rust game logic — zero external deps beyond rand/serde
|
||||||
├── solitaire_sync/ # Shared API types — used by client and server
|
├── solitaire_sync/ # Shared API types — used by client and server
|
||||||
├── solitaire_data/ # Persistence, sync client, settings
|
├── solitaire_data/ # Persistence, sync client, settings
|
||||||
├── solitaire_engine/ # Bevy ECS systems, components, plugins
|
├── solitaire_engine/ # Bevy ECS systems, components, plugins
|
||||||
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite)
|
├── 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
|
└── solitaire_app/ # Main binary entry point
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -135,25 +129,10 @@ Owns:
|
|||||||
- `SyncBackend` enum and backend selection
|
- `SyncBackend` enum and backend selection
|
||||||
- Solitaire Server sync client (JWT auth, auto-refresh)
|
- Solitaire Server sync client (JWT auth, auto-refresh)
|
||||||
- OS keychain integration (`keyring`)
|
- OS keychain integration (`keyring`)
|
||||||
- `SyncProvider` trait — implemented by both `SolitaireServerClient` and `GpgsClient` (Android)
|
- `SyncProvider` trait — implemented by `SolitaireServerClient`
|
||||||
|
|
||||||
### `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.
|
|
||||||
|
|
||||||
### `solitaire_engine`
|
### `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.
|
All Bevy-specific code. Structured as a collection of Plugins that `solitaire_app` registers.
|
||||||
|
|
||||||
@@ -162,9 +141,10 @@ Owns:
|
|||||||
- Rendering systems (card sprites, table, backgrounds)
|
- Rendering systems (card sprites, table, backgrounds)
|
||||||
- Drag-and-drop input handling
|
- Drag-and-drop input handling
|
||||||
- Animation systems (slide, flip, win cascade, toast)
|
- 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
|
- Audio playback systems
|
||||||
- Sync status display
|
- 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`
|
### `solitaire_server`
|
||||||
**Dependencies:** `solitaire_sync`, `axum`, `sqlx`, `jsonwebtoken`, `bcrypt`, `tower-governor`, `tracing`, `tokio`, `dotenvy`.
|
**Dependencies:** `solitaire_sync`, `axum`, `sqlx`, `jsonwebtoken`, `bcrypt`, `tower-governor`, `tracing`, `tokio`, `dotenvy`.
|
||||||
@@ -209,7 +189,7 @@ RenderSystem ScoreSystem AchievementSystem
|
|||||||
│
|
│
|
||||||
│ fires AchievementUnlockedEvent
|
│ fires AchievementUnlockedEvent
|
||||||
▼
|
▼
|
||||||
ToastSystem (egui popup)
|
ToastSystem (Bevy UI popup)
|
||||||
PersistenceSystem (write to disk)
|
PersistenceSystem (write to disk)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -223,8 +203,7 @@ SyncPlugin::on_startup()
|
|||||||
│ spawns AsyncComputeTask
|
│ spawns AsyncComputeTask
|
||||||
▼
|
▼
|
||||||
solitaire_data::sync_pull() ← dispatches to active SyncProvider
|
solitaire_data::sync_pull() ← dispatches to active SyncProvider
|
||||||
│ SolitaireServerClient (desktop / iOS)
|
│ SolitaireServerClient
|
||||||
│ GpgsClient (Android, future)
|
|
||||||
▼
|
▼
|
||||||
solitaire_sync::merge(local, remote)
|
solitaire_sync::merge(local, remote)
|
||||||
│
|
│
|
||||||
@@ -245,7 +224,7 @@ SyncPlugin::on_exit()
|
|||||||
│ blocking push (acceptable on exit, not on main loop)
|
│ blocking push (acceptable on exit, not on main loop)
|
||||||
▼
|
▼
|
||||||
active SyncProvider::push(local)
|
active SyncProvider::push(local)
|
||||||
│ POST to server — or — GPGS Saved Games PUT (Android)
|
│ POST to server
|
||||||
▼
|
▼
|
||||||
Done
|
Done
|
||||||
```
|
```
|
||||||
@@ -256,16 +235,38 @@ Done
|
|||||||
|
|
||||||
### Bevy Plugins
|
### Bevy Plugins
|
||||||
|
|
||||||
| Plugin | Responsibility |
|
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.
|
||||||
|---|---|
|
|
||||||
| `CardPlugin` | Card entity spawning, sprite management, drag-and-drop |
|
| Plugin | Shortcut | Responsibility |
|
||||||
| `TablePlugin` | Pile markers, background, layout calculation |
|
|---|---|---|
|
||||||
| `AnimationPlugin` | Slide, flip, win cascade, toast animations |
|
| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop |
|
||||||
| `AudioPlugin` | Sound effect and music playback via bevy_kira_audio |
|
| `TablePlugin` | — | Pile markers, background, layout calculation |
|
||||||
| `UIPlugin` | All egui screens: Home, Stats, Achievements, Settings, Profile |
|
| `FontPlugin` | — | Loads FiraMono-Medium via `AssetServer` at startup; exposes `FontResource` handle |
|
||||||
| `AchievementPlugin` | Listens for game events, evaluates unlock conditions, fires toasts |
|
| `AnimationPlugin` | — | Slide, flip, win cascade, toast animations |
|
||||||
| `SyncPlugin` | Manages sync lifecycle (pull on start, push on exit, status display) |
|
| `FeedbackAnimPlugin` | — | Shake, settle, and deal-stagger animations |
|
||||||
| `GamePlugin` | Core game state resource, input routing, win detection |
|
| `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
|
### Key Bevy Resources
|
||||||
|
|
||||||
@@ -290,6 +291,20 @@ struct StatsResource(StatsSnapshot);
|
|||||||
struct ProgressResource(PlayerProgress);
|
struct ProgressResource(PlayerProgress);
|
||||||
struct AchievementsResource(Vec<AchievementRecord>);
|
struct AchievementsResource(Vec<AchievementRecord>);
|
||||||
struct SettingsResource(Settings);
|
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
|
### Key Bevy Events
|
||||||
@@ -363,7 +378,6 @@ Implementations:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `LocalOnlyProvider` | No-op (default) | All |
|
| `LocalOnlyProvider` | No-op (default) | All |
|
||||||
| `SolitaireServerClient` | Self-hosted server | 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.
|
Sync always runs on `bevy::tasks::AsyncComputeTaskPool` — the game thread is never blocked.
|
||||||
|
|
||||||
@@ -378,9 +392,6 @@ pub enum SyncBackend {
|
|||||||
// JWT access + refresh tokens stored in OS keychain
|
// JWT access + refresh tokens stored in OS keychain
|
||||||
// key: "solitaire_quest_server_{username}"
|
// 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 +403,6 @@ On exit: `POST /api/sync/push` with payload
|
|||||||
On 401: automatically attempt `POST /api/auth/refresh`, retry once, then surface error to user.
|
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.
|
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
|
## 7. Sync Server Architecture
|
||||||
@@ -482,89 +489,7 @@ This ensures all players worldwide get the same challenge for a given date, rega
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Google Play Games Services (Android Future)
|
## 8. Data Models
|
||||||
|
|
||||||
> **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
|
|
||||||
|
|
||||||
### Core Game Models (`solitaire_core`)
|
### Core Game Models (`solitaire_core`)
|
||||||
|
|
||||||
@@ -588,6 +513,9 @@ pub enum PileType {
|
|||||||
|
|
||||||
pub enum DrawMode { DrawOne, DrawThree }
|
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 {
|
pub enum MoveError {
|
||||||
InvalidSource,
|
InvalidSource,
|
||||||
InvalidDestination,
|
InvalidDestination,
|
||||||
@@ -600,13 +528,16 @@ pub enum MoveError {
|
|||||||
pub struct GameState {
|
pub struct GameState {
|
||||||
pub piles: HashMap<PileType, Vec<Card>>,
|
pub piles: HashMap<PileType, Vec<Card>>,
|
||||||
pub draw_mode: DrawMode,
|
pub draw_mode: DrawMode,
|
||||||
|
pub mode: GameMode,
|
||||||
pub score: i32,
|
pub score: i32,
|
||||||
pub move_count: u32,
|
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 elapsed_seconds: u64,
|
||||||
pub seed: u64,
|
pub seed: u64,
|
||||||
pub is_won: bool,
|
pub is_won: bool,
|
||||||
pub is_auto_completable: 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 +583,14 @@ pub struct Settings {
|
|||||||
pub music_volume: f32,
|
pub music_volume: f32,
|
||||||
pub animation_speed: AnimSpeed,
|
pub animation_speed: AnimSpeed,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
pub sync_backend: SyncBackend, // Local | SolitaireServer | GooglePlayGames
|
pub sync_backend: SyncBackend, // Local | SolitaireServer
|
||||||
pub first_run_complete: bool,
|
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`).
|
All endpoints are under the base URL configured by the user (e.g., `https://solitaire.example.com`).
|
||||||
|
|
||||||
@@ -702,9 +633,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
|
```rust
|
||||||
pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
||||||
@@ -744,7 +675,7 @@ pub fn merge(local: &SyncPayload, remote: &SyncPayload) -> SyncPayload {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Achievement System
|
## 11. Achievement System
|
||||||
|
|
||||||
### Definition Structure
|
### Definition Structure
|
||||||
|
|
||||||
@@ -784,18 +715,17 @@ pub struct AchievementDef {
|
|||||||
| `speed_and_skill` | ??? | Win < 90s without undo | Yes | Card back #4 |
|
| `speed_and_skill` | ??? | Win < 90s without undo | Yes | Card back #4 |
|
||||||
| `comeback` | ??? | Win after 3+ stock recycles | Yes | Background #4 |
|
| `comeback` | ??? | Win after 3+ stock recycles | Yes | Background #4 |
|
||||||
| `zen_winner` | ??? | Win in Zen Mode | Yes | Badge |
|
| `zen_winner` | ??? | Win in Zen Mode | Yes | Badge |
|
||||||
|
| `cinephile` | Cinephile | Watch a saved replay all the way through | No | — |
|
||||||
|
|
||||||
### Evaluation Timing
|
### Evaluation Timing
|
||||||
|
|
||||||
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.
|
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)*
|
A small number of achievements are *event-driven* rather than condition-driven: their `AchievementDef::condition` always returns `false` and their unlock is written from a dedicated observer system instead. `cinephile` is the canonical example — it unlocks when `ReplayPlaybackState` transitions from `Playing` to `Completed` (a saved replay watched to its natural end). The Stop button transitions `Playing → Inactive` directly without entering `Completed`, so manual aborts do not unlock the achievement.
|
||||||
|
|
||||||
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
|
### XP Sources
|
||||||
|
|
||||||
@@ -824,62 +754,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 |
|
| File | Trigger |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `card_deal.ogg` | New game deal animation |
|
| `card_deal.wav` | New game deal animation |
|
||||||
| `card_flip.ogg` | Card flips face-up |
|
| `card_flip.wav` | Card flips face-up |
|
||||||
| `card_place.ogg` | Valid card placement |
|
| `card_place.wav` | Valid card placement |
|
||||||
| `card_invalid.ogg` | Invalid move attempt |
|
| `card_invalid.wav` | Invalid move attempt |
|
||||||
| `win_fanfare.ogg` | Game won |
|
| `win_fanfare.wav` | Game won |
|
||||||
| `ambient_loop.ogg` | Looping background music (restarts seamlessly) |
|
| `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.
|
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:
|
Backgrounds are Bevy `Sprite` entities with `Handle<Image>` from `BackgroundImageSet`. `BackgroundImageSet` is populated at startup by `table_plugin::load_background_images` via `AssetServer::load()`.
|
||||||
- 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
|
|
||||||
|
|
||||||
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 |
|
| Platform | Status | Primary Sync | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| macOS | Primary | Self-hosted server | x86_64 + Apple Silicon (universal binary via `cargo-lipo`) |
|
| 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+ |
|
| 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 |
|
| Android | Stretch | Self-hosted server | `cargo-mobile2`, touch input |
|
||||||
| iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input; GPGS unavailable on iOS |
|
| 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`.
|
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
|
### Prerequisites
|
||||||
|
|
||||||
@@ -940,7 +893,7 @@ Add `--features bevy/dynamic_linking` during development to dramatically reduce
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 18. Deployment Guide
|
## 17. Deployment Guide
|
||||||
|
|
||||||
### Docker Compose (Recommended)
|
### Docker Compose (Recommended)
|
||||||
|
|
||||||
@@ -985,7 +938,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 19. Security Model
|
## 18. Security Model
|
||||||
|
|
||||||
| Concern | Mitigation |
|
| Concern | Mitigation |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -1001,7 +954,7 @@ Migrations run automatically on startup via `sqlx::migrate!()`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 20. Testing Strategy
|
## 19. Testing Strategy
|
||||||
|
|
||||||
### Unit Tests (`solitaire_core`)
|
### Unit Tests (`solitaire_core`)
|
||||||
|
|
||||||
@@ -1040,12 +993,10 @@ Using `axum::test` and an in-memory SQLite database:
|
|||||||
- [ ] Achievement toast appears and dismisses
|
- [ ] Achievement toast appears and dismisses
|
||||||
- [ ] Server sync: register, login, push, pull on second machine
|
- [ ] Server sync: register, login, push, pull on second machine
|
||||||
- [ ] Server sync: JWT refresh on 401 works transparently
|
- [ ] 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 |
|
| Decision | Rationale | Date |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -1057,7 +1008,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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| `SyncProvider` trait, not `SyncBackend` match arms | `SyncPlugin` stays backend-agnostic and testable; new backends can be added without touching the plugin | 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 |
|
|
||||||
| Dropped WebDAV backend | Redundant once the self-hosted server exists; removing it reduces surface area and simplifies settings UI | 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 |
|
||||||
|
|||||||
@@ -1,113 +1,571 @@
|
|||||||
# Solitaire Quest — Claude Code Instructions
|
# CLAUDE.md
|
||||||
|
|
||||||
See @ARCHITECTURE.md for full project design, crate responsibilities, data models, and API reference.
|
version: unified-3.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Layout
|
# 0. Role of This File
|
||||||
|
|
||||||
```text
|
This document defines:
|
||||||
solitaire_core/ # Pure Rust game logic — NO Bevy, NO network, NO I/O
|
|
||||||
solitaire_sync/ # Shared API types — NO Bevy, serde/uuid/chrono only
|
* **Execution rules (what Claude must do)**
|
||||||
solitaire_data/ # Persistence + SyncProvider trait + server client
|
* **System constraints (what Claude must never violate)**
|
||||||
solitaire_engine/ # Bevy ECS systems, components, plugins
|
* **Operational architecture (how code is structured)**
|
||||||
solitaire_server/ # Axum sync server binary
|
|
||||||
solitaire_gpgs/ # Google Play Games bridge — STUB ONLY until Android phase
|
For full system design details:
|
||||||
solitaire_app/ # Thin binary entry point
|
→ `ARCHITECTURE.md` (authoritative source of truth)
|
||||||
assets/ # Source assets — embedded at compile time via include_bytes!()
|
|
||||||
|
This file overrides all conversational assumptions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. System Architecture (Authoritative Mapping)
|
||||||
|
|
||||||
|
## 1.1 Crates
|
||||||
|
|
||||||
|
```text id="crate_map"
|
||||||
|
solitaire_core/ # PURE logic (no IO, no Bevy, deterministic)
|
||||||
|
solitaire_sync/ # Shared API + merge logic
|
||||||
|
solitaire_data/ # Persistence + sync client
|
||||||
|
solitaire_engine/ # Bevy ECS + UI + gameplay orchestration
|
||||||
|
solitaire_server/ # Axum backend (optional sync layer)
|
||||||
|
solitaire_app/ # Entry binary
|
||||||
|
assets/ # Runtime assets (except audio)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Build & Test Commands
|
## 1.2 Architecture Source of Truth
|
||||||
|
|
||||||
```bash
|
* Full system design: `ARCHITECTURE.md`
|
||||||
# Dev run (fast compile via dynamic linking)
|
* This file NEVER redefines system design
|
||||||
cargo run -p solitaire_app --features bevy/dynamic_linking
|
* This file ONLY enforces behavior
|
||||||
|
|
||||||
# Release build
|
---
|
||||||
cargo build --workspace --release
|
|
||||||
|
|
||||||
# All tests — MUST pass before any commit
|
# 2. Hard Global Constraints (NON-NEGOTIABLE)
|
||||||
|
|
||||||
|
These override all other instructions.
|
||||||
|
|
||||||
|
## 2.1 Core Determinism
|
||||||
|
|
||||||
|
* `solitaire_core` MUST:
|
||||||
|
|
||||||
|
* be deterministic
|
||||||
|
* be side-effect free
|
||||||
|
* never depend on Bevy / IO / async
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2 Sync Isolation
|
||||||
|
|
||||||
|
* `solitaire_sync`:
|
||||||
|
|
||||||
|
* no Bevy
|
||||||
|
* no IO
|
||||||
|
* no engine dependencies
|
||||||
|
* merge logic must be pure functions only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.3 Error Policy
|
||||||
|
|
||||||
|
* NO `unwrap()`
|
||||||
|
* NO `panic!()` in runtime/game logic
|
||||||
|
* All state transitions:
|
||||||
|
|
||||||
|
```rust id="err_model"
|
||||||
|
Result<T, MoveError>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.4 Threading Rules
|
||||||
|
|
||||||
|
* Sync must run on `AsyncComputeTaskPool`
|
||||||
|
* NEVER block Bevy main thread
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.5 Persistence Rules
|
||||||
|
|
||||||
|
* atomic writes only:
|
||||||
|
|
||||||
|
* write `.tmp`
|
||||||
|
* rename atomically
|
||||||
|
* no partial state writes allowed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.6 Security Rules
|
||||||
|
|
||||||
|
* credentials ONLY via `keyring`
|
||||||
|
* NEVER store secrets in:
|
||||||
|
|
||||||
|
* files
|
||||||
|
* logs
|
||||||
|
* source code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.7 Sync System Rules
|
||||||
|
|
||||||
|
* All sync backends implement:
|
||||||
|
|
||||||
|
```rust id="sync_trait"
|
||||||
|
trait SyncProvider
|
||||||
|
```
|
||||||
|
|
||||||
|
* `SyncPlugin` MUST be backend-agnostic
|
||||||
|
* NEVER match on backend inside ECS systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Engine Rules (Bevy Layer)
|
||||||
|
|
||||||
|
## 3.1 ECS Design
|
||||||
|
|
||||||
|
* systems = single responsibility
|
||||||
|
* communication = Events only
|
||||||
|
* shared state = Resources only
|
||||||
|
* per-entity state = Components only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.2 Game State Authority
|
||||||
|
|
||||||
|
* ONLY `GameStateResource` can mutate game state
|
||||||
|
* UI systems MUST NOT directly modify core logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.3 UI-First Constraint (CRITICAL)
|
||||||
|
|
||||||
|
Every player action MUST:
|
||||||
|
|
||||||
|
* have a visible UI control
|
||||||
|
* NOT rely solely on keyboard shortcuts
|
||||||
|
|
||||||
|
Keyboard shortcuts are:
|
||||||
|
→ optional accelerators only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.4 Layout System
|
||||||
|
|
||||||
|
* recompute on `WindowResized`
|
||||||
|
* no fixed resolution assumptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Asset System Rules
|
||||||
|
|
||||||
|
## 4.1 Runtime Assets (AssetServer)
|
||||||
|
|
||||||
|
Loaded via:
|
||||||
|
|
||||||
|
* `CardImageSet`
|
||||||
|
* `BackgroundImageSet`
|
||||||
|
* `FontResource`
|
||||||
|
|
||||||
|
Includes:
|
||||||
|
|
||||||
|
* cards
|
||||||
|
* backgrounds
|
||||||
|
* fonts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2 Embedded Assets
|
||||||
|
|
||||||
|
Only audio:
|
||||||
|
|
||||||
|
```text id="audio_rule"
|
||||||
|
include_bytes!()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.3 Test Compatibility Rule
|
||||||
|
|
||||||
|
All asset loaders MUST accept:
|
||||||
|
|
||||||
|
```rust id="asset_fallback"
|
||||||
|
Option<Res<AssetServer>>
|
||||||
|
```
|
||||||
|
|
||||||
|
Must degrade gracefully under `MinimalPlugins`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Code Standards
|
||||||
|
|
||||||
|
## 5.1 Error Handling
|
||||||
|
|
||||||
|
* use `thiserror`
|
||||||
|
* no `Box<dyn Error>` in libraries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.2 Public API Rules
|
||||||
|
|
||||||
|
* prefer `Into<T>` over concrete types
|
||||||
|
* all public items require doc comments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.3 Derive Order
|
||||||
|
|
||||||
|
```rust id="derive_order"
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.4 Performance Rules
|
||||||
|
|
||||||
|
* NO `clone()` in hot paths
|
||||||
|
* profile before optimizing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.5 SQL Rules
|
||||||
|
|
||||||
|
* ONLY `sqlx::query!`
|
||||||
|
* NO raw SQL strings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Build & Verification Rules
|
||||||
|
|
||||||
|
These are mandatory before ANY commit.
|
||||||
|
|
||||||
|
```bash id="build_rules"
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
|
|
||||||
# Lint — MUST pass clean (zero warnings)
|
|
||||||
cargo clippy --workspace -- -D warnings
|
cargo clippy --workspace -- -D warnings
|
||||||
|
|
||||||
# Run sync server locally
|
|
||||||
cargo run -p solitaire_server
|
|
||||||
|
|
||||||
# Check a single crate
|
|
||||||
cargo test -p solitaire_core
|
|
||||||
cargo clippy -p solitaire_core -- -D warnings
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Hard Rules
|
# 7. Git Workflow Rules
|
||||||
|
|
||||||
- `solitaire_core` and `solitaire_sync` must never gain Bevy or network dependencies.
|
## Commit format
|
||||||
- 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`.
|
```text id="commit_fmt"
|
||||||
- Atomic file writes only: write to `filename.json.tmp`, then `rename()`.
|
type(scope): description
|
||||||
- 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.
|
Examples:
|
||||||
- `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.
|
* feat(core): add draw-three rules
|
||||||
- `cargo test --workspace` must pass after every change.
|
* fix(engine): correct drag z-order
|
||||||
|
* test(core): undo boundary cases
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Code Style
|
## Commit conditions
|
||||||
|
|
||||||
- Use `thiserror` for error types. Never `Box<dyn Error>` in library crates.
|
* tests must pass
|
||||||
- Prefer `Into<T>` over concrete types in public API function parameters.
|
* clippy must be clean
|
||||||
- All public items must have doc comments (`///`). Private items: comment only when non-obvious.
|
|
||||||
- Derive order convention: `#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]`
|
NEVER commit otherwise
|
||||||
- Bevy systems: one responsibility per system. Use `Events` for cross-system communication, never shared mutable state.
|
|
||||||
- SQL queries: use `sqlx::query!` macros (compile-time checked), not raw string queries.
|
|
||||||
- No `clone()` calls in hot paths (game loop systems). Profile before optimising elsewhere.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Bevy Conventions
|
# 8. Change Control (ASK BEFORE DOING)
|
||||||
|
|
||||||
- One `Plugin` per major feature: `CardPlugin`, `AudioPlugin`, `AchievementPlugin`, `UIPlugin`, `SyncPlugin`.
|
Claude must request confirmation before:
|
||||||
- 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.
|
* adding dependencies
|
||||||
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
|
* modifying `solitaire_sync`
|
||||||
|
* changing DB schema
|
||||||
|
* introducing `unsafe`
|
||||||
|
* changing merge strategy
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Git Workflow
|
# 9. System Mental Model (IMPORTANT)
|
||||||
|
|
||||||
- Commit after each passing phase, not after every file change.
|
```text id="mental_model"
|
||||||
- Commit message format: `type(scope): description`
|
Core (rules + deterministic logic)
|
||||||
- `feat(core): add draw-three mode validation`
|
↓
|
||||||
- `fix(engine): card z-order during drag`
|
Engine (Bevy orchestration)
|
||||||
- `test(core): undo stack boundary conditions`
|
↓
|
||||||
- `chore(server): add sqlx migration 002`
|
Data layer (persistence + sync)
|
||||||
- Never commit with failing tests or clippy warnings.
|
↓
|
||||||
- Never commit secrets, `.env` files, or `*.db` files.
|
Server (optional external system)
|
||||||
|
```
|
||||||
|
|
||||||
|
Core is always the source of truth.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ask Before Doing
|
# 10. Known Platform Pitfalls
|
||||||
|
|
||||||
- Adding a new crate dependency (discuss alternatives first).
|
Must always be handled explicitly:
|
||||||
- Changing a type in `solitaire_sync` (breaking change on both client and server).
|
|
||||||
- Altering the database schema (requires a new sqlx migration).
|
* Bevy `Time` uses `f32`
|
||||||
- Introducing `unsafe` code anywhere.
|
* `sqlx::migrate!()` path is crate-relative
|
||||||
- Changing the merge strategy in `solitaire_sync::merge()`.
|
* `dirs::data_dir()` may return `None`
|
||||||
|
* Linux may lack keyring backend
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Lessons Learned
|
# 11. Forbidden Patterns
|
||||||
|
|
||||||
> Add entries here when Claude makes a mistake so it isn't repeated.
|
* game logic inside Bevy systems
|
||||||
|
* duplication across crates
|
||||||
|
* blocking async calls in ECS
|
||||||
|
* insecure credential storage
|
||||||
|
* bypassing core logic layer
|
||||||
|
|
||||||
- Bevy's `Time` resource uses `f32` seconds; convert to `u64` only when writing to `StatsSnapshot`.
|
---
|
||||||
- `sqlx::migrate!()` macro path is relative to the crate root, not the workspace root.
|
|
||||||
- `keyring` on Linux requires a running secret service (e.g. GNOME Keyring or KWallet) — handle `Error::NoStorageAccess` gracefully and fall back to prompting the user.
|
# 12. Execution Rules for Claude
|
||||||
- `dirs::data_dir()` returns `None` on some minimal Linux environments — always handle the `None` case explicitly, do not unwrap.
|
|
||||||
|
When generating code:
|
||||||
|
|
||||||
|
1. respect crate boundaries
|
||||||
|
2. minimize diff size
|
||||||
|
3. do not expand scope
|
||||||
|
4. follow existing patterns
|
||||||
|
5. preserve invariants
|
||||||
|
|
||||||
|
If unclear:
|
||||||
|
→ ask before acting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 13. Relationship to ARCHITECTURE.md
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| --------------- | ------------------------- |
|
||||||
|
| CLAUDE.md | execution + constraints |
|
||||||
|
| ARCHITECTURE.md | system design truth |
|
||||||
|
| Both combined | full system understanding |
|
||||||
|
|
||||||
|
---
|
||||||
|
# 14. Context Injection System (AUTOMATIC SCOPE FILTER)
|
||||||
|
|
||||||
|
## 14.1 Purpose
|
||||||
|
|
||||||
|
Before generating any response, Claude MUST construct a **minimal relevant context set**.
|
||||||
|
|
||||||
|
This prevents:
|
||||||
|
|
||||||
|
* architectural drift
|
||||||
|
* irrelevant spec loading
|
||||||
|
* over-engineering
|
||||||
|
* cross-crate confusion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.2 Input Classification Step (MANDATORY)
|
||||||
|
|
||||||
|
Every request MUST be classified into exactly one task type:
|
||||||
|
|
||||||
|
```text id="task_types"
|
||||||
|
feature
|
||||||
|
bugfix
|
||||||
|
refactor
|
||||||
|
system_design
|
||||||
|
bevy_system
|
||||||
|
core_logic
|
||||||
|
sync
|
||||||
|
optimization
|
||||||
|
test
|
||||||
|
debug
|
||||||
|
```
|
||||||
|
|
||||||
|
If uncertain → ask clarification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.3 Context Selection Engine
|
||||||
|
|
||||||
|
After classification, Claude MUST include ONLY the relevant sections below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.4 Context Map (CORE RULESET)
|
||||||
|
|
||||||
|
### feature
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* §2 Hard Global Constraints
|
||||||
|
* §3 Engine Rules
|
||||||
|
* ARCHITECTURE.md (crate of target feature only)
|
||||||
|
* relevant data models (GameState, SyncPayload if needed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### bugfix
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* §2 Hard Global Constraints
|
||||||
|
* §5 Code Standards
|
||||||
|
* affected crate boundaries
|
||||||
|
* relevant system (engine/core/sync only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### refactor
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* §3 Engine Rules
|
||||||
|
* §5 Code Standards
|
||||||
|
* §11 Forbidden Patterns
|
||||||
|
* target crate boundaries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### system_design
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* ARCHITECTURE.md (FULL)
|
||||||
|
* §9 Mental Model
|
||||||
|
* §1 System Architecture Mapping
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### core_logic
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* solitaire_core rules only
|
||||||
|
* GameState model
|
||||||
|
* MoveError model
|
||||||
|
* §2.1–2.3 constraints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### bevy_system
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* §3 Engine Rules
|
||||||
|
* ECS rules (Events/Resources/Components)
|
||||||
|
* UI-first constraint
|
||||||
|
* relevant plugin system only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### sync
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* SyncProvider trait
|
||||||
|
* merge strategy rules
|
||||||
|
* solitaire_sync models
|
||||||
|
* §2.6 Sync Rules
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### optimization
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* target crate only
|
||||||
|
* §5.4 Performance Rules
|
||||||
|
* hot path constraints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### test
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* §6 Build Rules
|
||||||
|
* relevant module
|
||||||
|
* expected invariants
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### debug
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
* target file/module only
|
||||||
|
* §2.3 Error Policy
|
||||||
|
* runtime assumptions relevant to failure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.5 Context Compression Rules
|
||||||
|
|
||||||
|
Claude MUST obey:
|
||||||
|
|
||||||
|
* never include full ARCHITECTURE.md unless system_design
|
||||||
|
* max 2 crates per response unless explicitly required
|
||||||
|
* prefer function-level context over file-level context
|
||||||
|
* exclude unrelated plugins/systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.6 Context Priority Order
|
||||||
|
|
||||||
|
When space is limited:
|
||||||
|
|
||||||
|
1. Hard Constraints (§2)
|
||||||
|
2. Target crate rules
|
||||||
|
3. Data models
|
||||||
|
4. Only then: architecture snippets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.7 “No Context Pollution” Rule
|
||||||
|
|
||||||
|
Claude must NOT include:
|
||||||
|
|
||||||
|
* unrelated crates
|
||||||
|
* unrelated plugins
|
||||||
|
* unused data models
|
||||||
|
* full architecture dumps
|
||||||
|
* speculative systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.8 Self-Check Before Execution
|
||||||
|
|
||||||
|
Before writing code, Claude MUST verify:
|
||||||
|
|
||||||
|
* [ ] Is only relevant context included?
|
||||||
|
* [ ] Is at least one hard constraint present?
|
||||||
|
* [ ] Am I touching more than one crate unnecessarily?
|
||||||
|
* [ ] Am I duplicating ARCHITECTURE.md content?
|
||||||
|
|
||||||
|
If any fail → revise context selection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.9 Injection Output Format (Internal Model)
|
||||||
|
|
||||||
|
Claude should behave as if it constructed:
|
||||||
|
|
||||||
|
```text id="ctx_format"
|
||||||
|
[SELECTED TASK TYPE]
|
||||||
|
|
||||||
|
[MINIMAL REQUIRED RULES]
|
||||||
|
|
||||||
|
[MINIMAL ARCHITECTURE SLICES]
|
||||||
|
|
||||||
|
[RELEVANT MODELS]
|
||||||
|
|
||||||
|
[REQUEST]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14.10 Relationship to ARCHITECTURE.md
|
||||||
|
|
||||||
|
* ARCHITECTURE.md = source of truth
|
||||||
|
* CLAUDE.md = execution constraints
|
||||||
|
* THIS SECTION = filtering layer between them
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# END CONTEXT INJECTION SYSTEM
|
||||||
|
|||||||
@@ -0,0 +1,497 @@
|
|||||||
|
# CLAUDE_PROMPT_PACK.md
|
||||||
|
|
||||||
|
version: 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 0. GLOBAL INSTRUCTION (prepend to every prompt)
|
||||||
|
|
||||||
|
```
|
||||||
|
You must follow CLAUDE_SPEC.md strictly.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Do not expand scope beyond what is defined
|
||||||
|
- Do not refactor unrelated code
|
||||||
|
- Do not introduce new dependencies
|
||||||
|
- Prefer minimal, surgical changes
|
||||||
|
- Use existing patterns in the codebase
|
||||||
|
- Return minimal diffs or changed functions only
|
||||||
|
|
||||||
|
Before writing code:
|
||||||
|
1. List relevant constraints from CLAUDE_SPEC.md
|
||||||
|
2. Identify risks
|
||||||
|
3. Then implement
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. FEATURE IMPLEMENTATION
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Feature Implementation
|
||||||
|
|
||||||
|
feature: "<name>"
|
||||||
|
|
||||||
|
goal:
|
||||||
|
"<clear outcome>"
|
||||||
|
|
||||||
|
scope:
|
||||||
|
crates: []
|
||||||
|
systems: []
|
||||||
|
files: []
|
||||||
|
|
||||||
|
non_goals:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
- must follow CLAUDE_SPEC.md
|
||||||
|
- event-driven architecture required
|
||||||
|
- no blocking operations
|
||||||
|
- no cross-crate leakage
|
||||||
|
|
||||||
|
acceptance_criteria:
|
||||||
|
- ""
|
||||||
|
- ""
|
||||||
|
|
||||||
|
edge_cases:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required Patterns
|
||||||
|
|
||||||
|
Use this pattern for systems:
|
||||||
|
<PASTE EXISTING SYSTEM SNIPPET HERE>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
intent:
|
||||||
|
plan:
|
||||||
|
constraints_used:
|
||||||
|
risks:
|
||||||
|
|
||||||
|
code_changes:
|
||||||
|
(minimal diffs only)
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. BUGFIX
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Bug Fix
|
||||||
|
|
||||||
|
bug_description:
|
||||||
|
"<what is broken>"
|
||||||
|
|
||||||
|
expected_behavior:
|
||||||
|
"<correct behavior>"
|
||||||
|
|
||||||
|
root_cause_hint (optional):
|
||||||
|
""
|
||||||
|
|
||||||
|
scope:
|
||||||
|
crates: []
|
||||||
|
files: []
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
- minimal fix only
|
||||||
|
- no refactors unless required
|
||||||
|
- must add regression protection if applicable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. Identify root cause
|
||||||
|
2. Fix it minimally
|
||||||
|
3. Preserve all invariants
|
||||||
|
4. Do not change unrelated logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
analysis:
|
||||||
|
root_cause:
|
||||||
|
fix_strategy:
|
||||||
|
|
||||||
|
code_changes:
|
||||||
|
(minimal diff)
|
||||||
|
|
||||||
|
regression_test (only if high-value):
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. REFACTOR
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Refactor
|
||||||
|
|
||||||
|
target:
|
||||||
|
"<what is being improved>"
|
||||||
|
|
||||||
|
goal:
|
||||||
|
"<what improves>"
|
||||||
|
|
||||||
|
scope:
|
||||||
|
crates: []
|
||||||
|
files: []
|
||||||
|
|
||||||
|
non_goals:
|
||||||
|
- no behavior changes
|
||||||
|
- no new features
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
- must preserve behavior exactly
|
||||||
|
- must respect crate boundaries
|
||||||
|
- must not duplicate logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Refactor Type
|
||||||
|
|
||||||
|
- [ ] simplify logic
|
||||||
|
- [ ] reduce duplication
|
||||||
|
- [ ] improve readability
|
||||||
|
- [ ] performance (non-invasive)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
analysis:
|
||||||
|
issues_found:
|
||||||
|
|
||||||
|
refactor_plan:
|
||||||
|
|
||||||
|
code_changes:
|
||||||
|
(diff only)
|
||||||
|
|
||||||
|
verification:
|
||||||
|
- behavior unchanged: yes/no
|
||||||
|
- invariants preserved: yes/no
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. SYSTEM DESIGN (NEW FEATURE)
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: System Design
|
||||||
|
|
||||||
|
feature:
|
||||||
|
"<name>"
|
||||||
|
|
||||||
|
goal:
|
||||||
|
"<what problem it solves>"
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
- must fit existing architecture
|
||||||
|
- must follow plugin + event model
|
||||||
|
- must not violate crate boundaries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required Output
|
||||||
|
|
||||||
|
design:
|
||||||
|
|
||||||
|
components:
|
||||||
|
- plugins:
|
||||||
|
- systems:
|
||||||
|
- events:
|
||||||
|
- resources:
|
||||||
|
|
||||||
|
data_flow:
|
||||||
|
(step-by-step)
|
||||||
|
|
||||||
|
integration_points:
|
||||||
|
- where it connects to existing systems
|
||||||
|
|
||||||
|
risks:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
tradeoffs:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DO NOT
|
||||||
|
|
||||||
|
- write full implementation
|
||||||
|
- modify unrelated systems
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. NEW BEVY SYSTEM
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Add Bevy System
|
||||||
|
|
||||||
|
system_name:
|
||||||
|
""
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
(event or condition)
|
||||||
|
|
||||||
|
reads:
|
||||||
|
[Resources]
|
||||||
|
|
||||||
|
writes:
|
||||||
|
[Resources]
|
||||||
|
|
||||||
|
emits:
|
||||||
|
[Events]
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
- must be event-driven
|
||||||
|
- must not directly mutate unrelated state
|
||||||
|
- must be single responsibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
system_signature:
|
||||||
|
|
||||||
|
implementation:
|
||||||
|
(code only)
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. CORE LOGIC FUNCTION (solitaire_core)
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Core Logic Implementation
|
||||||
|
|
||||||
|
function:
|
||||||
|
"<name>"
|
||||||
|
|
||||||
|
goal:
|
||||||
|
"<what it does>"
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- no IO
|
||||||
|
- no async
|
||||||
|
- no Bevy
|
||||||
|
- deterministic
|
||||||
|
|
||||||
|
invariants:
|
||||||
|
- ""
|
||||||
|
- ""
|
||||||
|
|
||||||
|
errors:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
constraints_checked:
|
||||||
|
|
||||||
|
implementation:
|
||||||
|
(code only)
|
||||||
|
|
||||||
|
edge_case_handling:
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. SYNC / MERGE LOGIC
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Sync Logic
|
||||||
|
|
||||||
|
goal:
|
||||||
|
"<what is being merged or synced>"
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
- must be deterministic
|
||||||
|
- must be idempotent
|
||||||
|
- must be lossless
|
||||||
|
- must not delete data
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- counters → max
|
||||||
|
- times → min
|
||||||
|
- collections → union
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
analysis:
|
||||||
|
|
||||||
|
merge_logic:
|
||||||
|
|
||||||
|
code_changes:
|
||||||
|
|
||||||
|
invariants_verified:
|
||||||
|
- deterministic
|
||||||
|
- idempotent
|
||||||
|
- lossless
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. PERFORMANCE OPTIMIZATION
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Optimization
|
||||||
|
|
||||||
|
target:
|
||||||
|
"<what is slow>"
|
||||||
|
|
||||||
|
constraints:CLAUDE_WORKFLOW.md
|
||||||
|
- no behavior change
|
||||||
|
- no architecture change
|
||||||
|
- minimal code changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
analysis:
|
||||||
|
bottleneck:
|
||||||
|
|
||||||
|
optimization_strategy:
|
||||||
|
|
||||||
|
code_changes:
|
||||||
|
|
||||||
|
impact_estimate:
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. TEST GENERATION (STRICT MODE)
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Test Generation
|
||||||
|
|
||||||
|
target:
|
||||||
|
"<function/system>"
|
||||||
|
|
||||||
|
reason:
|
||||||
|
- bugfix | complex logic | invariant protection
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
- no redundant tests
|
||||||
|
- must test real behavior
|
||||||
|
- must fail if logic breaks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
test_cases:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
test_code:
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. DEBUGGING / INVESTIGATION
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Debug
|
||||||
|
|
||||||
|
problem:
|
||||||
|
"<symptom>"
|
||||||
|
|
||||||
|
context:
|
||||||
|
"<relevant code or system>"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required Steps
|
||||||
|
|
||||||
|
1. List possible causes
|
||||||
|
2. Narrow down most likely
|
||||||
|
3. Suggest verification steps
|
||||||
|
4. Provide minimal fix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
hypotheses:
|
||||||
|
|
||||||
|
most_likely:
|
||||||
|
|
||||||
|
verification_steps:
|
||||||
|
|
||||||
|
fix:
|
||||||
|
|
||||||
|
notes:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. HARD CONSTRAINT OVERRIDE (RARE)
|
||||||
|
|
||||||
|
```
|
||||||
|
# TASK: Exception Handling
|
||||||
|
|
||||||
|
reason:
|
||||||
|
"<why constraints must be bent>"
|
||||||
|
|
||||||
|
requested_exception:
|
||||||
|
"<rule being broken>"
|
||||||
|
|
||||||
|
justification:
|
||||||
|
"<why unavoidable>"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
analysis:
|
||||||
|
|
||||||
|
alternatives_considered:
|
||||||
|
|
||||||
|
final_decision:
|
||||||
|
|
||||||
|
risk:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. STOP CONDITIONS (always append)
|
||||||
|
|
||||||
|
```
|
||||||
|
Stop when:
|
||||||
|
- acceptance criteria are met
|
||||||
|
- code is minimal and correct
|
||||||
|
|
||||||
|
Do NOT:
|
||||||
|
- expand scope
|
||||||
|
- refactor unrelated code
|
||||||
|
- optimize prematurely
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# END
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
# CLAUDE_SPEC.md
|
||||||
|
|
||||||
|
version: 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Global Rules
|
||||||
|
|
||||||
|
(Core determinism, panic policy, and event-driven engine constraints live in CLAUDE.md §2.1, §2.3, §3.1. Listed here only when they add information CLAUDE.md doesn't carry.)
|
||||||
|
|
||||||
|
rules:
|
||||||
|
|
||||||
|
* id: single_source_of_truth
|
||||||
|
description: "GameStateResource is the only mutable game state in runtime"
|
||||||
|
|
||||||
|
* id: sync_is_additive
|
||||||
|
description: "Remote data must never destructively overwrite local data"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Crate Graph
|
||||||
|
|
||||||
|
crates:
|
||||||
|
solitaire_core:
|
||||||
|
depends_on: [rand, serde, chrono]
|
||||||
|
forbidden_deps: [bevy, reqwest, tokio, std::fs]
|
||||||
|
|
||||||
|
solitaire_sync:
|
||||||
|
depends_on: [serde, serde_json, uuid, chrono]
|
||||||
|
role: "shared_types"
|
||||||
|
|
||||||
|
solitaire_data:
|
||||||
|
depends_on: [solitaire_core, solitaire_sync, reqwest, tokio, keyring]
|
||||||
|
role: "persistence_and_sync"
|
||||||
|
|
||||||
|
solitaire_engine:
|
||||||
|
depends_on: [bevy, kira, solitaire_core, solitaire_data]
|
||||||
|
role: "runtime_engine"
|
||||||
|
|
||||||
|
solitaire_server:
|
||||||
|
depends_on: [solitaire_sync, axum, sqlx, jsonwebtoken]
|
||||||
|
role: "backend"
|
||||||
|
|
||||||
|
solitaire_app:
|
||||||
|
depends_on: [solitaire_engine]
|
||||||
|
role: "entrypoint"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Data Ownership
|
||||||
|
|
||||||
|
ownership:
|
||||||
|
GameState:
|
||||||
|
owner: solitaire_core
|
||||||
|
mutable_in: solitaire_engine
|
||||||
|
access_pattern: "via GameStateResource only"
|
||||||
|
|
||||||
|
StatsSnapshot:
|
||||||
|
owner: solitaire_data
|
||||||
|
|
||||||
|
PlayerProgress:
|
||||||
|
owner: solitaire_data
|
||||||
|
|
||||||
|
AchievementRecord:
|
||||||
|
owner: solitaire_data
|
||||||
|
|
||||||
|
SyncPayload:
|
||||||
|
owner: solitaire_sync
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. State Transitions
|
||||||
|
|
||||||
|
state_machine:
|
||||||
|
GameState:
|
||||||
|
transitions:
|
||||||
|
- action: move_cards
|
||||||
|
returns: Result<GameState, MoveError>
|
||||||
|
|
||||||
|
```
|
||||||
|
- action: draw
|
||||||
|
returns: Result<GameState, MoveError>
|
||||||
|
|
||||||
|
- action: undo
|
||||||
|
returns: Result<GameState, MoveError>
|
||||||
|
|
||||||
|
invariants:
|
||||||
|
- "52 cards always exist"
|
||||||
|
- "no duplicate card IDs"
|
||||||
|
- "all cards belong to exactly one pile"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Event System
|
||||||
|
|
||||||
|
events:
|
||||||
|
|
||||||
|
input:
|
||||||
|
- MoveRequestEvent
|
||||||
|
- DrawRequestEvent
|
||||||
|
- UndoRequestEvent
|
||||||
|
- NewGameRequestEvent
|
||||||
|
|
||||||
|
state:
|
||||||
|
- StateChangedEvent
|
||||||
|
- GameWonEvent
|
||||||
|
|
||||||
|
meta:
|
||||||
|
- AchievementUnlockedEvent
|
||||||
|
- SyncCompleteEvent
|
||||||
|
|
||||||
|
rules:
|
||||||
|
|
||||||
|
* "Input events trigger core logic"
|
||||||
|
* "Core logic emits state events"
|
||||||
|
* "UI reacts to state events only"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sync Contract
|
||||||
|
|
||||||
|
sync:
|
||||||
|
|
||||||
|
provider_trait:
|
||||||
|
methods:
|
||||||
|
- pull() -> SyncPayload
|
||||||
|
- push(payload) -> SyncResponse
|
||||||
|
|
||||||
|
guarantees:
|
||||||
|
- "non-blocking during gameplay"
|
||||||
|
- "blocking allowed on exit only"
|
||||||
|
|
||||||
|
merge:
|
||||||
|
rules:
|
||||||
|
counters: "max"
|
||||||
|
best_times: "min"
|
||||||
|
collections: "union"
|
||||||
|
achievements: "never removed"
|
||||||
|
|
||||||
|
```
|
||||||
|
properties:
|
||||||
|
- deterministic
|
||||||
|
- idempotent
|
||||||
|
- lossless
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Persistence
|
||||||
|
|
||||||
|
storage:
|
||||||
|
|
||||||
|
format: json
|
||||||
|
|
||||||
|
files:
|
||||||
|
- stats.json
|
||||||
|
- progress.json
|
||||||
|
- achievements.json
|
||||||
|
- settings.json
|
||||||
|
- game_state.json
|
||||||
|
|
||||||
|
guarantees:
|
||||||
|
- atomic_write: true
|
||||||
|
- crash_safe: true
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Engine Rules
|
||||||
|
|
||||||
|
engine:
|
||||||
|
|
||||||
|
mutation_rules:
|
||||||
|
- "Only GameLogicSystem mutates GameState"
|
||||||
|
- "UI systems are read-only"
|
||||||
|
|
||||||
|
threading:
|
||||||
|
- "sync runs on AsyncComputeTaskPool"
|
||||||
|
- "main thread must never block"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
pattern: "feature_isolation"
|
||||||
|
communication: "events"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Server Contract
|
||||||
|
|
||||||
|
server:
|
||||||
|
|
||||||
|
auth:
|
||||||
|
method: jwt
|
||||||
|
access_expiry: 24h
|
||||||
|
refresh_expiry: 30d
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- POST /api/auth/register
|
||||||
|
- POST /api/auth/login
|
||||||
|
- GET /api/sync/pull
|
||||||
|
- POST /api/sync/push
|
||||||
|
|
||||||
|
limits:
|
||||||
|
payload_max: 1MB
|
||||||
|
rate_limit: "10 req/min auth routes"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Achievement System
|
||||||
|
|
||||||
|
achievements:
|
||||||
|
|
||||||
|
definition_location: solitaire_core
|
||||||
|
state_location: solitaire_data
|
||||||
|
|
||||||
|
types:
|
||||||
|
- condition_based
|
||||||
|
- event_driven
|
||||||
|
|
||||||
|
rule:
|
||||||
|
- "achievements cannot be revoked"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Testing Rules
|
||||||
|
|
||||||
|
testing:
|
||||||
|
|
||||||
|
philosophy:
|
||||||
|
- "test real failures"
|
||||||
|
- "avoid redundant tests"
|
||||||
|
|
||||||
|
required_coverage:
|
||||||
|
solitaire_core:
|
||||||
|
- move_validation
|
||||||
|
- undo_integrity
|
||||||
|
- win_detection
|
||||||
|
|
||||||
|
```
|
||||||
|
solitaire_sync:
|
||||||
|
- merge_correctness
|
||||||
|
- idempotency
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Prohibited Patterns
|
||||||
|
|
||||||
|
(See CLAUDE.md §11 for the canonical forbidden-patterns list.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Extension Points
|
||||||
|
|
||||||
|
extensibility:
|
||||||
|
|
||||||
|
sync_backends:
|
||||||
|
pattern: "implement SyncProvider"
|
||||||
|
|
||||||
|
game_modes:
|
||||||
|
location: solitaire_core::GameMode
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
rule: "new feature = new plugin"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Validation Checklist (for Claude)
|
||||||
|
|
||||||
|
validation:
|
||||||
|
|
||||||
|
* check: "crate dependency rules respected"
|
||||||
|
* check: "no panics in core"
|
||||||
|
* check: "events used for cross-system communication"
|
||||||
|
* check: "GameState mutations centralized"
|
||||||
|
* check: "merge function properties preserved"
|
||||||
|
* check: "no blocking operations in main loop"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Mental Model
|
||||||
|
|
||||||
|
model:
|
||||||
|
|
||||||
|
layers:
|
||||||
|
- core
|
||||||
|
- engine
|
||||||
|
- data
|
||||||
|
- server
|
||||||
|
|
||||||
|
flow:
|
||||||
|
- input -> engine -> core -> engine -> ui
|
||||||
|
- data <-> sync <-> server
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# CLAUDE_WORKFLOW.md
|
||||||
|
|
||||||
|
version: 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Overview
|
||||||
|
|
||||||
|
This workflow defines a **two-agent system**:
|
||||||
|
|
||||||
|
* **Builder Agent** → writes and modifies code
|
||||||
|
* **Guardian Agent** → enforces architecture + rejects invalid changes
|
||||||
|
|
||||||
|
No code is considered valid unless it passes Guardian validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Agent Roles
|
||||||
|
|
||||||
|
### 1.1 Builder Agent
|
||||||
|
|
||||||
|
role: "code_generation"
|
||||||
|
|
||||||
|
responsibilities:
|
||||||
|
|
||||||
|
* implement features
|
||||||
|
* refactor code
|
||||||
|
* generate tests (only when justified)
|
||||||
|
* follow CLAUDE_SPEC.md
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
|
||||||
|
* cannot bypass validation
|
||||||
|
* must declare intent before writing code
|
||||||
|
|
||||||
|
output_contract:
|
||||||
|
must_produce:
|
||||||
|
- change_summary
|
||||||
|
- files_modified
|
||||||
|
- reasoning (short)
|
||||||
|
- code_diff
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 Guardian Agent
|
||||||
|
|
||||||
|
role: "architecture_enforcement"
|
||||||
|
|
||||||
|
responsibilities:
|
||||||
|
|
||||||
|
* validate against CLAUDE_SPEC.md
|
||||||
|
* detect violations
|
||||||
|
* reject or approve changes
|
||||||
|
* suggest minimal fixes (not full rewrites)
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
|
||||||
|
* no feature implementation
|
||||||
|
* no large rewrites
|
||||||
|
* must be deterministic
|
||||||
|
|
||||||
|
output_contract:
|
||||||
|
must_produce:
|
||||||
|
- status: APPROVED | REJECTED
|
||||||
|
- violations[]
|
||||||
|
- required_fixes[]
|
||||||
|
- optional_improvements[]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Workflow Pipeline
|
||||||
|
|
||||||
|
```text
|
||||||
|
User Request
|
||||||
|
↓
|
||||||
|
Builder Agent (proposal + code)
|
||||||
|
↓
|
||||||
|
Guardian Agent (validation)
|
||||||
|
↓
|
||||||
|
IF approved → commit
|
||||||
|
IF rejected → feedback → Builder retry
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Builder Protocol
|
||||||
|
|
||||||
|
### Step 1 — Intent Declaration
|
||||||
|
|
||||||
|
Builder MUST start with:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
intent:
|
||||||
|
feature: "<name>"
|
||||||
|
crates_touched: []
|
||||||
|
systems_affected: []
|
||||||
|
risk_level: low|medium|high
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2 — Plan
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plan:
|
||||||
|
- step: "..."
|
||||||
|
- step: "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3 — Implementation
|
||||||
|
|
||||||
|
* Only modify declared crates
|
||||||
|
* Follow ownership rules
|
||||||
|
* Use events for cross-system communication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4 — Output
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
change_summary: "..."
|
||||||
|
|
||||||
|
files_modified:
|
||||||
|
- path: ...
|
||||||
|
change: "..."
|
||||||
|
|
||||||
|
violations_self_check:
|
||||||
|
- none | list
|
||||||
|
|
||||||
|
notes: "short reasoning"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Guardian Protocol
|
||||||
|
|
||||||
|
### Step 1 — Spec Validation
|
||||||
|
|
||||||
|
Check against:
|
||||||
|
|
||||||
|
* crate boundaries
|
||||||
|
* mutation rules
|
||||||
|
* event system usage
|
||||||
|
* sync guarantees
|
||||||
|
* forbidden patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2 — Invariant Validation
|
||||||
|
|
||||||
|
Must verify:
|
||||||
|
|
||||||
|
* GameState invariants preserved
|
||||||
|
* no new panic paths
|
||||||
|
* no blocking calls in engine
|
||||||
|
* merge properties unchanged
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3 — Output Decision
|
||||||
|
|
||||||
|
#### APPROVED
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
status: APPROVED
|
||||||
|
|
||||||
|
notes:
|
||||||
|
- "no violations"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### REJECTED
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
status: REJECTED
|
||||||
|
|
||||||
|
violations:
|
||||||
|
- id: core_purity_violation
|
||||||
|
file: "solitaire_core/src/..."
|
||||||
|
reason: "uses std::fs"
|
||||||
|
|
||||||
|
required_fixes:
|
||||||
|
- "move IO to solitaire_data"
|
||||||
|
|
||||||
|
optional_improvements:
|
||||||
|
- "simplify event naming"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Enforcement Rules
|
||||||
|
|
||||||
|
### Hard Fail (automatic rejection)
|
||||||
|
|
||||||
|
* core crate uses IO / Bevy / network
|
||||||
|
* GameState mutated outside GameLogicSystem
|
||||||
|
* blocking async on main thread
|
||||||
|
* duplicate logic across crates
|
||||||
|
* merge function altered incorrectly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Soft Fail (allowed but flagged)
|
||||||
|
|
||||||
|
* unnecessary complexity
|
||||||
|
* redundant tests
|
||||||
|
* minor architectural drift
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Iteration Loop
|
||||||
|
|
||||||
|
Max attempts per task: **3**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Attempt 1 → Reject → Fix
|
||||||
|
Attempt 2 → Reject → Fix
|
||||||
|
Attempt 3 → Final decision
|
||||||
|
```
|
||||||
|
|
||||||
|
If still failing:
|
||||||
|
→ escalate to user
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Diff Strategy
|
||||||
|
|
||||||
|
Builder MUST produce:
|
||||||
|
|
||||||
|
* minimal diffs
|
||||||
|
* no unrelated refactors
|
||||||
|
* no formatting-only changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Test Strategy Integration
|
||||||
|
|
||||||
|
Builder rules:
|
||||||
|
|
||||||
|
* only add tests if:
|
||||||
|
|
||||||
|
* fixing a bug
|
||||||
|
* protecting complex logic
|
||||||
|
* validating invariants
|
||||||
|
|
||||||
|
Guardian rejects:
|
||||||
|
|
||||||
|
* redundant tests
|
||||||
|
* no-op tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Optional Extensions
|
||||||
|
|
||||||
|
### 9.1 Third Agent (Optimizer)
|
||||||
|
|
||||||
|
role: performance + cleanup
|
||||||
|
|
||||||
|
runs AFTER approval:
|
||||||
|
|
||||||
|
* reduce allocations
|
||||||
|
* simplify logic
|
||||||
|
* improve ECS scheduling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.2 CI Integration
|
||||||
|
|
||||||
|
Pipeline:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Builder → Guardian → cargo check → clippy → tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Guardian runs BEFORE compilation to catch structural issues early.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Example Interaction
|
||||||
|
|
||||||
|
### Builder
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
intent:
|
||||||
|
feature: "undo stack limit fix"
|
||||||
|
crates_touched: [solitaire_core]
|
||||||
|
risk_level: low
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
change_summary: "limit undo stack to 64 entries"
|
||||||
|
|
||||||
|
files_modified:
|
||||||
|
- solitaire_core/src/game_state.rs
|
||||||
|
|
||||||
|
notes: "prevents unbounded memory growth"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Guardian
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
status: APPROVED
|
||||||
|
|
||||||
|
notes:
|
||||||
|
- "respects core constraints"
|
||||||
|
- "no invariant violations"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Mental Model
|
||||||
|
|
||||||
|
* Builder = **creative**
|
||||||
|
* Guardian = **strict**
|
||||||
|
|
||||||
|
Builder explores
|
||||||
|
Guardian enforces
|
||||||
|
|
||||||
|
Neither replaces the other.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Success Criteria
|
||||||
|
|
||||||
|
System is working if:
|
||||||
|
|
||||||
|
* architectural violations go to ~0
|
||||||
|
* code stays consistent across features
|
||||||
|
* refactors become safe
|
||||||
|
* complexity grows sub-linearly
|
||||||
@@ -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,42 +5,121 @@ members = [
|
|||||||
"solitaire_data",
|
"solitaire_data",
|
||||||
"solitaire_engine",
|
"solitaire_engine",
|
||||||
"solitaire_server",
|
"solitaire_server",
|
||||||
"solitaire_gpgs",
|
|
||||||
"solitaire_app",
|
"solitaire_app",
|
||||||
"solitaire_assetgen",
|
"solitaire_assetgen",
|
||||||
|
"solitaire_wasm",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
license = "MIT"
|
||||||
|
rust-version = "1.95"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
rand = "0.8"
|
rand = "0.9"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
dirs = "5"
|
dirs = "6"
|
||||||
keyring = "2"
|
keyring = "4"
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
keyring-core = "1"
|
||||||
|
reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
|
||||||
|
arboard = { version = "3", default-features = false }
|
||||||
|
|
||||||
solitaire_core = { path = "solitaire_core" }
|
solitaire_core = { path = "solitaire_core" }
|
||||||
solitaire_sync = { path = "solitaire_sync" }
|
solitaire_sync = { path = "solitaire_sync" }
|
||||||
solitaire_data = { path = "solitaire_data" }
|
solitaire_data = { path = "solitaire_data" }
|
||||||
solitaire_engine = { path = "solitaire_engine" }
|
solitaire_engine = { path = "solitaire_engine" }
|
||||||
|
|
||||||
bevy = "0.15"
|
# Bevy with `default-features = false` to avoid the unused
|
||||||
kira = "0.9"
|
# `bevy_audio → rodio + symphonia + cpal 0.15 + alsa 0.9` chain.
|
||||||
|
# Audio is handled directly by `kira` in `audio_plugin.rs`, so the
|
||||||
|
# `bevy_audio` feature is intentionally omitted. The features below
|
||||||
|
# enumerate every leaf of the standard `2d` + `ui` meta-features that
|
||||||
|
# we actually use; new features should only be added with a
|
||||||
|
# corresponding use site.
|
||||||
|
bevy = { version = "0.18", default-features = false, features = [
|
||||||
|
# default_app
|
||||||
|
"async_executor",
|
||||||
|
"bevy_asset",
|
||||||
|
"bevy_input_focus",
|
||||||
|
"bevy_log",
|
||||||
|
"bevy_state",
|
||||||
|
"bevy_window",
|
||||||
|
"custom_cursor",
|
||||||
|
"reflect_auto_register",
|
||||||
|
# default_platform (desktop subset)
|
||||||
|
"std",
|
||||||
|
"bevy_winit",
|
||||||
|
"default_font",
|
||||||
|
"multi_threaded",
|
||||||
|
# winit prefers Wayland when WAYLAND_DISPLAY is set on the
|
||||||
|
# session and falls through to X11 otherwise. Without `wayland`,
|
||||||
|
# winit-on-Wayland-session falls back to XWayland which renders
|
||||||
|
# the game in an X11 frame inside the Wayland compositor.
|
||||||
|
"wayland",
|
||||||
|
"x11",
|
||||||
|
# Android: NativeActivity glue. The feature is target-gated inside
|
||||||
|
# bevy_internal — desktop builds compile it out, so leaving it on
|
||||||
|
# the always-on list is harmless on Linux/macOS/Windows. Pairs with
|
||||||
|
# cargo-apk's NativeActivity wrapper (cargo-apk 0.10+ uses this by
|
||||||
|
# default). Switch to `android-game-activity` later if we want
|
||||||
|
# AndroidX GameActivity for Google Play Games integration.
|
||||||
|
"android-native-activity",
|
||||||
|
# common_api
|
||||||
|
"bevy_color",
|
||||||
|
"bevy_image",
|
||||||
|
"bevy_mesh",
|
||||||
|
"bevy_shader",
|
||||||
|
"bevy_text",
|
||||||
|
"png",
|
||||||
|
# 2d rendering
|
||||||
|
"bevy_camera",
|
||||||
|
"bevy_render",
|
||||||
|
"bevy_core_pipeline",
|
||||||
|
"bevy_sprite",
|
||||||
|
"bevy_sprite_render",
|
||||||
|
# UI rendering
|
||||||
|
"bevy_ui",
|
||||||
|
"bevy_ui_render",
|
||||||
|
] }
|
||||||
|
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"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = { version = "10", default-features = false, features = ["rust_crypto"] }
|
||||||
bcrypt = "0.15"
|
bcrypt = "0.19"
|
||||||
tower_governor = "0.4"
|
tower_governor = "0.8"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
dotenvy = "0.15"
|
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
|
||||||
|
- **19 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,668 @@
|
|||||||
|
# Solitaire Quest — Session Handoff
|
||||||
|
|
||||||
|
**Last updated:** 2026-05-08 — v0.20.0 cut and tagged at `41a009a`,
|
||||||
|
all post-cut commits pushed to origin (HEAD = `dd101b3`), working
|
||||||
|
tree clean.
|
||||||
|
The cut itself shipped two through-lines: a full **Terminal visual-
|
||||||
|
identity port** (token system, modal scaffold, gameplay-feedback,
|
||||||
|
toasts, table / card chrome, splash cursor) and the **Android
|
||||||
|
persistence shim** that closes the `dirs::data_dir() = None` pitfall
|
||||||
|
flagged in CLAUDE.md §10. Since the cut, the post-tag work split
|
||||||
|
into two arcs: (1) splash boot-screen port + replay-overlay
|
||||||
|
banner enrichments + desktop-adaptation spec — closing Resume-prompt
|
||||||
|
Options B and C (see "Since the v0.20.0 cut" entries below); and
|
||||||
|
(2) **the card-face artwork regeneration arc — Option D, closed
|
||||||
|
2026-05-08** — full Terminal cards rendering on every face, plus
|
||||||
|
three follow-up fixes that surfaced during sign-off (default-theme
|
||||||
|
SVG override, table backgrounds, top-bar overlap), plus a
|
||||||
|
glyph-orientation tweak (no 180° inverted-corner rotation).
|
||||||
|
|
||||||
|
## Status at pause
|
||||||
|
|
||||||
|
- **HEAD locally:** see `git rev-parse HEAD`. Most recent narrative
|
||||||
|
entry below names the latest substantive commit; this status line
|
||||||
|
intentionally avoids hard-coding the SHA so a docs-only edit
|
||||||
|
doesn't immediately stale the handoff.
|
||||||
|
- **HEAD on origin:** matches local. All post-cut commits pushed
|
||||||
|
through `dd101b3`. Decide whether to roll the post-tag work
|
||||||
|
into v0.20.1 / v0.21.0-candidates the next time a release is cut.
|
||||||
|
- **Working tree:** clean. No WIP outstanding.
|
||||||
|
- **`artwork/` directory:** still untracked. Intentional.
|
||||||
|
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
|
||||||
|
clean.
|
||||||
|
- **Tests:** **1184 passing / 0 failing** across the workspace.
|
||||||
|
Net delta from the 1180 baseline: splash polish added two
|
||||||
|
(`build_scanline_image_has_expected_2x2_rgba_bytes`,
|
||||||
|
`scanline_overlay_spawns_and_fades_with_splash`); the
|
||||||
|
card-face migration added one (`card_face_svg_pin` integration
|
||||||
|
test) and consolidated two (`face_colour` CBM tests folded
|
||||||
|
into `text_colour` CBM tests, net −2 then +1 from pin);
|
||||||
|
call it +4 net.
|
||||||
|
- **Tags on origin:** `v0.9.0` through `v0.20.0`. v0.20.0 is on
|
||||||
|
`41a009a`.
|
||||||
|
|
||||||
|
## Since the v0.20.0 cut (un-pushed)
|
||||||
|
|
||||||
|
### `39b8496` `docs(ui): add Terminal desktop-adaptation spec`
|
||||||
|
|
||||||
|
`docs/ui-mockups/desktop-adaptation.md` — 283 lines covering
|
||||||
|
viewport assumptions, seven universal adaptation rules, and per-
|
||||||
|
screen geometry rules for the priority surfaces (Game Table, Win
|
||||||
|
Summary, Settings, Help, Pause, Home, Splash, Stats, and the
|
||||||
|
modal-pattern screens Profile / Achievements / Theme Picker /
|
||||||
|
Daily Challenge). Closes the spec gap — 23 of 24 mockups were
|
||||||
|
mobile-only, but the v0.20.0 token-port pass was already layout-
|
||||||
|
agnostic so nothing shipped broken. The spec matters for *next*
|
||||||
|
ports.
|
||||||
|
|
||||||
|
**Why rules > visual mockups for this gap:** Stitch's
|
||||||
|
`generate_variants` API timed out on the layout-only adaptation
|
||||||
|
prompt (server-side flake, not a prompt-shape issue — confirmed
|
||||||
|
by polling `list_screens` with no new variant landing). A markdown
|
||||||
|
rules file applies to every screen including the 9 missing-plugin
|
||||||
|
surfaces (splash, challenge, time-attack, weekly-goals,
|
||||||
|
leaderboard, sync, level-up, replay-overlay, radial-menu) that
|
||||||
|
aren't in the Stitch project at all. It's also referenceable from
|
||||||
|
code comments and commit messages without loading an image.
|
||||||
|
|
||||||
|
### `cacb19c` `feat(engine): port the splash to the Terminal boot-screen treatment`
|
||||||
|
|
||||||
|
Implements the full mockup-spec splash from
|
||||||
|
`docs/ui-mockups/splash-mobile.html` plus the desktop adaptation
|
||||||
|
rules:
|
||||||
|
|
||||||
|
- **Header**: cursor block (96 px `▌`), wordmark ("Solitaire
|
||||||
|
Quest"), 192 px divider, "TERMINAL EDITION" subtitle.
|
||||||
|
- **Boot log**: three ✓ check rows (`assets loaded`,
|
||||||
|
`theme: terminal`, `progress restored`) + a `▌ ready_` line.
|
||||||
|
Capped at 480 px width on desktop (else 70 % viewport).
|
||||||
|
- **Progress bar**: 1 px track (`BORDER_SUBTLE`) with a 100 %-
|
||||||
|
width cyan (`ACCENT_PRIMARY`) fill + `DONE · 247 ASSETS`
|
||||||
|
caption. Capped at 720 px on desktop (else 80 %).
|
||||||
|
- **Footer**: `BASE16-EIGHTIES` label, eight palette swatches
|
||||||
|
(12 × 12 px each — one per named token in the design system),
|
||||||
|
version line.
|
||||||
|
|
||||||
|
**Refactored the alpha-fade scaffold** from per-marker queries
|
||||||
|
(`SplashTitle` / `SplashSubtitle` / `SplashCursor`) to a single
|
||||||
|
`SplashFadable { base_color: Color }` + `SplashFadableBg`
|
||||||
|
variant. ~15 fadable elements share one global query each;
|
||||||
|
adding more is one component-attach, not three new query types.
|
||||||
|
|
||||||
|
**Skipped, with rationale captured in the commit:**
|
||||||
|
- Scanline overlay (needs a tiled-pattern asset or custom shader).
|
||||||
|
*Open in "Visual-identity follow-ups" below.*
|
||||||
|
- Pulsing cursor on the "ready_" line (would fight the global
|
||||||
|
fade timeline). *Open in "Visual-identity follow-ups" below.*
|
||||||
|
- "RUSTY SOLITAIRE" wordmark from the mockup (the actual product
|
||||||
|
is "Solitaire Quest"; the mockup leaked the repo name). *Closed
|
||||||
|
— the in-engine wordmark stays "Solitaire Quest".*
|
||||||
|
|
||||||
|
### `c84d9f4` `feat(engine): scrub fill bar + per-frame updater for replay overlay`
|
||||||
|
|
||||||
|
Closes the WIP described in the prior handoff. Adds the 1 px cyan
|
||||||
|
scrub bar called for in `docs/ui-mockups/replay-overlay-mobile.html`:
|
||||||
|
a track in `BORDER_SUBTLE` spans the bottom edge of the banner and
|
||||||
|
the cyan `ACCENT_PRIMARY` fill mirrors `cursor / total` via a new
|
||||||
|
`ReplayOverlayScrubFill` component + `update_scrub_fill` system.
|
||||||
|
The pure `scrub_pct` helper is shared between the spawn path
|
||||||
|
(initial fill width) and the per-frame updater so the first paint
|
||||||
|
already reflects state instead of popping `0 → cursor` on the
|
||||||
|
first tick — same shape as the existing `format_progress` /
|
||||||
|
`update_progress_text` split. Two new tests cover the four corners
|
||||||
|
of `scrub_pct` and an end-to-end drive of `ReplayPlaybackState`
|
||||||
|
asserting `Node.width` on the unique scrub-fill entity. Same
|
||||||
|
change-detection guard as the text updaters, so an idle replay
|
||||||
|
leaves the node untouched.
|
||||||
|
|
||||||
|
Header text treatment (closed by `6204db8` immediately below),
|
||||||
|
move-log scroll, MOVE chip, and WIN MOVE callout from the same
|
||||||
|
mockup are still open — separate commits.
|
||||||
|
|
||||||
|
### `6204db8` `feat(engine): port replay banner label to ▌ cursor-block treatment`
|
||||||
|
|
||||||
|
Aligns the replay overlay's headline with the splash boot-screen
|
||||||
|
idiom landed in `cacb19c`: `Replay` → `▌ replay` and
|
||||||
|
`Replay complete` → `▌ replay complete`. The cursor block (`▌`,
|
||||||
|
U+258C) prefixed to a lowercased label reads as a Terminal output
|
||||||
|
line rather than a generic UI title, tightening the family
|
||||||
|
resemblance between the two top-level overlay surfaces. Pure
|
||||||
|
text-content change; no behavioural shift, no new components, no
|
||||||
|
new systems.
|
||||||
|
|
||||||
|
**Mockup deviation (intentional):** the source mockup string in
|
||||||
|
`docs/ui-mockups/replay-overlay-mobile.html` is `▌replay.tsx`. The
|
||||||
|
`.tsx` is a prototyping leak — Stitch renders in React, so the
|
||||||
|
mockup author reached for a familiar filename — and was dropped
|
||||||
|
for the in-engine version since the codebase is Rust. The `▌` +
|
||||||
|
lowercase pattern is what reads as a Terminal-output-line; the
|
||||||
|
extension is incidental. (Same shape as the "RUSTY SOLITAIRE"
|
||||||
|
wordmark deviation noted under `cacb19c` — the mockup leaked the
|
||||||
|
repo name; the actual product is "Solitaire Quest".)
|
||||||
|
|
||||||
|
### `54005d5` `feat(engine): add GAME #YYYY-DDD caption beneath the replay headline`
|
||||||
|
|
||||||
|
Adds the right-anchored game-identifier piece of the replay-overlay
|
||||||
|
mockup, adapted to live *under* the existing "▌ replay" headline as
|
||||||
|
a `TYPE_CAPTION` (11 px) / `TEXT_SECONDARY` subtitle. Format is
|
||||||
|
`GAME #{year}-{ordinal:03}` (e.g. `GAME #2026-122` for a replay
|
||||||
|
recorded 2026-05-02) — year + chrono ordinal gives a compact,
|
||||||
|
monotonically-increasing identifier matching the mockup's
|
||||||
|
`GAME #2024-127` motif. New `ReplayOverlayGameCaption` marker, new
|
||||||
|
pure helper `format_game_caption(state) -> Option<String>` (None
|
||||||
|
for Inactive / Completed since the replay is consumed in those
|
||||||
|
branches; spawn-time fall-through to empty string).
|
||||||
|
|
||||||
|
**Layout impact:** `BANNER_HEIGHT` bumped 48 → 60 px so the new
|
||||||
|
left column (headline + 2 px gap + caption ≈ 39 px content) fits
|
||||||
|
under the scrub bar with room to spare. +12 px banner mass is the
|
||||||
|
deliberate cost of the new content; no other plugin observes
|
||||||
|
`BANNER_HEIGHT` so the change is local.
|
||||||
|
|
||||||
|
Two new tests (1180 → 1182): `format_game_caption_covers_state_corners`
|
||||||
|
pins the three branches plus the zero-pad-to-3-digits invariant
|
||||||
|
for early-January ordinals; `overlay_game_caption_shows_replay_date`
|
||||||
|
drives `ReplayPlaybackState` end-to-end.
|
||||||
|
|
||||||
|
### `e080b49` `feat(engine): restyle replay progress text as Terminal MOVE chip`
|
||||||
|
|
||||||
|
Closes the centre-text half of the replay-overlay enrichments. The
|
||||||
|
plain "Move N of M" text becomes a 1px `ACCENT_PRIMARY`-bordered
|
||||||
|
chip containing "MOVE N/M" — uppercase + slash separator reads as
|
||||||
|
a Terminal output line and matches the floating-chip motif in
|
||||||
|
`docs/ui-mockups/replay-overlay-mobile.html`. The chip lives
|
||||||
|
in-banner rather than floating above the focused card (the
|
||||||
|
screen-takeover treatment that requires plumbing cursor → card
|
||||||
|
identity remains deferred).
|
||||||
|
|
||||||
|
**Implementation note:** `BorderColor` in Bevy 0.18 is a per-side
|
||||||
|
struct, not a tuple — `BorderColor::all(ACCENT_PRIMARY)` is the
|
||||||
|
correct constructor. Worth pinning for next time we touch a
|
||||||
|
border-painted UI surface. The `ReplayOverlayProgressText` marker
|
||||||
|
stays on the inner Text rather than the new chip Node so
|
||||||
|
`update_progress_text` keeps repainting unchanged — a deliberate
|
||||||
|
"markers belong on the entity that updates change" choice.
|
||||||
|
|
||||||
|
Test count unchanged (1182); `overlay_progress_text_reflects_cursor`
|
||||||
|
swapped its assertion from "Move 5 of 10" to "MOVE 5/10".
|
||||||
|
|
||||||
|
This pair (`54005d5` + `e080b49`) closes Option C from the
|
||||||
|
SESSION_HANDOFF Resume prompt's banner-local enrichments. Floating-
|
||||||
|
chip-above-focused-card and the full screen-takeover redesign
|
||||||
|
remain — both data-layer or cross-plugin and intentionally still
|
||||||
|
open.
|
||||||
|
|
||||||
|
### `29136d8` `feat(engine): add pulsing trailing cursor to splash "▌ ready_" line`
|
||||||
|
|
||||||
|
Closes the cursor-pulse half of the splash polish arc deferred in
|
||||||
|
`cacb19c`. The "▌ ready_" line now ends with a 6×12 px cyan Node
|
||||||
|
that pulses on a 1 s sine cadence, multiplied with the global
|
||||||
|
splash fade timeline so the cursor never reaches full alpha while
|
||||||
|
the rest of the splash is still fading in.
|
||||||
|
|
||||||
|
**The "multiply, don't override" pattern.** Two systems write the
|
||||||
|
same `BackgroundColor` per frame: `advance_splash` writes the
|
||||||
|
global-fade alpha, `pulse_splash_cursor` overwrites with
|
||||||
|
`global_alpha × pulse_factor`. Both derive from `SplashAge` on the
|
||||||
|
root, so the writes are commensurate — the second one isn't
|
||||||
|
"fighting" the first, just refining it. This is the cleanest fix
|
||||||
|
for the "fight the global fade timeline" warning the original
|
||||||
|
`cacb19c` skip note flagged.
|
||||||
|
|
||||||
|
**Defensive division guard.** `cursor_pulse_factor(age, period, min)`
|
||||||
|
short-circuits to `1.0` when `period <= 0.0` so a future
|
||||||
|
misconfiguration produces a steady cursor rather than NaN
|
||||||
|
propagation (NaN in alpha = invisible UI, hard to debug). Worth
|
||||||
|
mirroring on every trig/division helper, not just this one.
|
||||||
|
|
||||||
|
One new test (1182 → 1183): `cursor_pulse_factor_corners` pins the
|
||||||
|
peak (factor = 1 at age = period / 4), trough (factor = min at age =
|
||||||
|
period × 3 / 4), and the zero/negative-period guard.
|
||||||
|
|
||||||
|
### `a27cf5a` `feat(engine): add tiled scanline overlay to splash`
|
||||||
|
|
||||||
|
Closes the scanline half of the splash polish arc. A fullscreen
|
||||||
|
`ImageNode` tiles a runtime-generated 2×2 RGBA8 texture over the
|
||||||
|
splash content — top row transparent, bottom row `#1a1a1a` at
|
||||||
|
~30 % alpha — producing the 1 px-pitch horizontal scanline pattern
|
||||||
|
called for in `docs/ui-mockups/splash-mobile.html`.
|
||||||
|
|
||||||
|
**Texture-α × tint-α composite for fade integration.** The 30 %
|
||||||
|
alpha is baked into the texture pixels, not the `ImageNode.color`
|
||||||
|
tint. `advance_splash`'s new third query writes
|
||||||
|
`(1, 1, 1, global_alpha)` into the tint each tick; the GPU
|
||||||
|
multiplies texture-α by tint-α, so the visible composite is
|
||||||
|
`0.3 × global_alpha`. Cleaner than building a "multiplicative
|
||||||
|
fadable" abstraction in the ECS — the GPU already does this
|
||||||
|
multiplication for free.
|
||||||
|
|
||||||
|
**Bevy 0.18 API surprises (worth pinning):**
|
||||||
|
- `RenderAssetUsages` re-exports under `bevy::asset::`, not
|
||||||
|
`bevy::render::render_asset::`. Type name unchanged; module
|
||||||
|
path moved.
|
||||||
|
- `TextureFormat::pixel_size()` returns `Result<usize, _>` rather
|
||||||
|
than the bare `usize` you'd expect for a static format query.
|
||||||
|
Annoying enough that the `debug_assert_eq!` against the buffer
|
||||||
|
length just hard-codes the `2 × 2 × 4 = 16` literal.
|
||||||
|
|
||||||
|
Headless test fixture now also `init_resource::<Assets<Image>>()`
|
||||||
|
since `MinimalPlugins` doesn't pull `AssetPlugin` — same pattern
|
||||||
|
`settings_plugin::tests` already used. Without it, the
|
||||||
|
`Option<ResMut<Assets<Image>>>` parameter on `spawn_splash` would
|
||||||
|
fall through and the scanline overlay would silently skip,
|
||||||
|
defeating the new tests.
|
||||||
|
|
||||||
|
Two new tests (1183 → 1185):
|
||||||
|
`build_scanline_image_has_expected_2x2_rgba_bytes` locks the
|
||||||
|
texture pixels literally so a future tweak can't drift the
|
||||||
|
appearance silently; `scanline_overlay_spawns_and_fades_with_splash`
|
||||||
|
asserts spawn placement under `SplashRoot` and the new
|
||||||
|
fade-images branch's correctness end-to-end.
|
||||||
|
|
||||||
|
This pair (`29136d8` + `a27cf5a`) closes Option B from the
|
||||||
|
SESSION_HANDOFF Resume prompt — both splash polish pieces now
|
||||||
|
shipped.
|
||||||
|
|
||||||
|
### `5623368`…`dd101b3` — Option D card-face migration arc
|
||||||
|
|
||||||
|
Closed 2026-05-08 across nine commits. The full Terminal card
|
||||||
|
artwork now renders end-to-end. Detail breakdown lives in the
|
||||||
|
"Visual-identity follow-ups" punch-list entry below; the short
|
||||||
|
version:
|
||||||
|
|
||||||
|
- Migration plan + pipeline tooling: `5623368` (plan doc),
|
||||||
|
`3a4bb63` (single-card PoC proving the `usvg`/`resvg` pipeline
|
||||||
|
at per-card grain), `babe5cc` (full
|
||||||
|
`solitaire_engine/examples/card_face_generator.rs` example
|
||||||
|
emitting 52 faces + 5 backs into `assets/cards/`), `48b28d2`
|
||||||
|
(the `card_face_svg_pin` integration test pinning rasteriser
|
||||||
|
output via inline FNV-1a hashing of raw RGBA8 bytes — the
|
||||||
|
pin's bootstrap pattern, "empty `EXPECTED` → run → paste",
|
||||||
|
is the maintenance interface for future intentional changes).
|
||||||
|
- Lockstep step 4+5: `e8bf9d7`. New PNG bytes + the 5
|
||||||
|
`card_plugin` constants (`CARD_FACE_COLOUR`,
|
||||||
|
`RED_SUIT_COLOUR`, `BLACK_SUIT_COLOUR`,
|
||||||
|
`CARD_FACE_COLOUR_RED_CBM` → `RED_SUIT_COLOUR_CBM`,
|
||||||
|
`card_back_colour`) + signature shifts in one commit.
|
||||||
|
`face_colour` deleted — Terminal face is uniformly
|
||||||
|
`CARD_FACE_COLOUR` regardless of CBM, so the function
|
||||||
|
collapsed to a constant. `text_colour` gained a
|
||||||
|
`color_blind: bool` parameter (red→cyan suit-glyph swap when
|
||||||
|
CBM is on). Four `face_colour` CBM tests folded into two
|
||||||
|
`text_colour` CBM tests in lockstep.
|
||||||
|
- Three follow-ups that surfaced during sign-off, all from the
|
||||||
|
same "fallback path the migration walked past" pattern:
|
||||||
|
`a14200a` regenerated the embedded **default-theme SVGs** at
|
||||||
|
`solitaire_engine/assets/themes/default/*.svg`; those bytes
|
||||||
|
`include_bytes!()`-embed into the binary and override
|
||||||
|
`assets/cards/*.png` at startup, so the PNG migration alone
|
||||||
|
didn't change what production rendered. `8719f77`
|
||||||
|
regenerated `assets/backgrounds/bg_*.png` to flat Terminal
|
||||||
|
near-black (5 solid-colour PNGs via a new
|
||||||
|
`solitaire_engine/examples/background_generator.rs` example).
|
||||||
|
`ae84dc1` cleared the **top-bar overlap** at portrait/narrow
|
||||||
|
window widths by swapping the action-button row's hardcoded
|
||||||
|
`font_size: 16.0` to `TYPE_BODY` (a typography-migration
|
||||||
|
miss) and stepping horizontal padding from `VAL_SPACE_3`
|
||||||
|
to `VAL_SPACE_2`.
|
||||||
|
- Glyph-rendering fix: `af414b6`. The bundled `FiraMono`
|
||||||
|
doesn't carry usable U+2660-2666 glyphs at the requested
|
||||||
|
size — `usvg` was silently substituting tiny "tofu" marks.
|
||||||
|
Switched suit glyphs from `<text>` elements to inline SVG
|
||||||
|
`<path>` elements via a new `suit_path_d` helper. Path-based
|
||||||
|
rendering bypasses the font system entirely; same bytes on
|
||||||
|
every machine, no fontdb dependency, no substitution risk.
|
||||||
|
Same path data renders correctly whether filled (♥ ♠) or
|
||||||
|
outlined (♦ ♣ — the always-on color-blind glyph
|
||||||
|
differentiation).
|
||||||
|
- Glyph-orientation tweak: `dd101b3`. Removed the 180° rotation
|
||||||
|
from the bottom-right large suit glyph at user request. Both
|
||||||
|
glyphs now render upright. `design-system.md` § Game Cards
|
||||||
|
line 220 updated in lockstep — the deliberate deviation from
|
||||||
|
the traditional inverted-corner-indicator convention is
|
||||||
|
documented in the spec, not just the code.
|
||||||
|
|
||||||
|
The pin test fired exactly twice during this arc (once for the
|
||||||
|
text→path switch, once for the unrotation) and rebaselined
|
||||||
|
cleanly each time via the empty-then-paste pattern. The 5
|
||||||
|
`back_*` hashes stayed identical across both rebaselines —
|
||||||
|
secondary signal that the FNV-1a fingerprinting is purely
|
||||||
|
deterministic on rasteriser output.
|
||||||
|
|
||||||
|
This arc closes Option D from the SESSION_HANDOFF Resume prompt
|
||||||
|
and effectively completes the Terminal visual-identity port —
|
||||||
|
only the toast warning/error variant slots remain wired-but-
|
||||||
|
unused.
|
||||||
|
|
||||||
|
## What shipped in v0.20.0 (frozen at `41a009a`)
|
||||||
|
|
||||||
|
### Terminal visual-identity port
|
||||||
|
|
||||||
|
Top-down stack — every commit downstream of the token system
|
||||||
|
reads from it, so swapping the palette is now a one-file edit:
|
||||||
|
|
||||||
|
- **`ui_theme` token system** (`0d477ac`). base16-eighties
|
||||||
|
palette, 5-rung type scale, 7-rung 4-multiple spacing scale,
|
||||||
|
3-step radius, 14-rung z-index hierarchy, full motion budget,
|
||||||
|
4 invariant-pinning unit tests. Card-shadow alphas pinned to 0
|
||||||
|
(Terminal achieves depth via 1px borders + tonal layering).
|
||||||
|
- **Modal scaffold already on tokens** — `ui_modal` was ported
|
||||||
|
in the same commit's wake; three stale "loud yellow" /
|
||||||
|
"magenta secondary" doc comments fixed.
|
||||||
|
- **Gameplay feedback → semantic state tokens** (`ceec4fc`).
|
||||||
|
Selection / valid-drop tints route through `ACCENT_PRIMARY` /
|
||||||
|
`STATE_WARNING` / `STATE_SUCCESS`.
|
||||||
|
- **Toasts** (`a137607`). New `ToastVariant` enum
|
||||||
|
(Info / Warning / Error / Celebration); opaque `BG_ELEVATED`
|
||||||
|
+ 1px accent border + bottom-anchor. All ten call sites pass
|
||||||
|
their semantic variant.
|
||||||
|
- **`table_plugin` chrome** (`651f406`).
|
||||||
|
`PILE_MARKER_DEFAULT_COLOUR` promoted; `cursor_plugin` imports
|
||||||
|
it, replacing a "kept in sync" doc comment with a compile-
|
||||||
|
enforced invariant. `HINT_PILE_HIGHLIGHT_COLOUR` →
|
||||||
|
`STATE_WARNING`.
|
||||||
|
- **`card_plugin` chrome** (`d752870`). Drag-elevation shadow
|
||||||
|
routes through `CARD_SHADOW_*` tokens. `RIGHT_CLICK_HIGHLIGHT_COLOUR`
|
||||||
|
→ `STATE_SUCCESS`. Stock recycle "↺" text → `TEXT_PRIMARY @ 0.7α`.
|
||||||
|
Card-face / suit / card-back palette intentionally NOT migrated
|
||||||
|
(artwork dependency — see open-list item below).
|
||||||
|
- **Splash cursor** (`cdcadda`). The signature `▌` cyan glyph
|
||||||
|
(96 px) added above the wordmark, matching the spec.
|
||||||
|
*Subsequently expanded post-cut by `cacb19c` into the full
|
||||||
|
boot-screen treatment.*
|
||||||
|
- **Hint-source / dest pairing** (`9891ae4`). `input_plugin`'s
|
||||||
|
source-card tint now matches the destination pile's
|
||||||
|
`STATE_WARNING`.
|
||||||
|
- **Design system + 24-mockup library** (`fa7f98a`).
|
||||||
|
`docs/ui-mockups/design-system.md` + 24 Stitch mockups (HTML +
|
||||||
|
PNG) covering every screen plus 9 missing-plugin surfaces.
|
||||||
|
- **`card_shadow_params` test aligned** (`1d1543e`). Drag-vs-
|
||||||
|
idle shadow assertion loosened to `>=` to accept the Terminal
|
||||||
|
"no shadow" intent without losing the regression-guard.
|
||||||
|
|
||||||
|
### Android persistence
|
||||||
|
|
||||||
|
- **`solitaire_data::data_dir` shim** (`4b51e50`). New
|
||||||
|
`solitaire_data::platform::data_dir()` falls through to
|
||||||
|
`dirs::data_dir()` on desktop and returns the per-app sandbox
|
||||||
|
at `/data/data/com.solitairequest.app/files` on Android — no
|
||||||
|
JNI needed (package id pinned in `[package.metadata.android]`).
|
||||||
|
Six `solitaire_data` callsites + `solitaire_engine/assets/user_dir.rs`
|
||||||
|
migrated. Settings, stats, achievements, replays, game-state,
|
||||||
|
time-attack sessions, and user themes now persist on Android.
|
||||||
|
|
||||||
|
### Inherited from earlier in the cycle (pre-session)
|
||||||
|
|
||||||
|
- Android build target + APK (`fb8b2ac`), runbook (`59424a3`),
|
||||||
|
F3 FPS overlay (`690e1d2`), Smart Window Size opt-out
|
||||||
|
(`e1b8766`), Shareable badge (`9b065e5`), Help cheat-sheet
|
||||||
|
M/P/Enter rows (`35516d3`), `pull_failure_sets_error_status`
|
||||||
|
flake fix (`67c150b`).
|
||||||
|
|
||||||
|
## Open punch list
|
||||||
|
|
||||||
|
### Phase Android (build + persistence shipped; runtime gaps remain)
|
||||||
|
|
||||||
|
- **APK launch verification on AVD / device.** `adb install` then
|
||||||
|
`adb logcat` against the `bevy_test` AVD or an x86_64 device.
|
||||||
|
The build works and persistence is wired, but no end-to-end
|
||||||
|
device run has been logged. Shakes out runtime bugs the build +
|
||||||
|
unit tests can't catch.
|
||||||
|
- **JNI ClipboardManager bridge.** Replaces the Android stub for
|
||||||
|
the Stats "Copy share link" toast. `arboard` doesn't ship an
|
||||||
|
Android backend; small custom JNI call.
|
||||||
|
- **Android Keystore for credentials.** `keyring` is target-gated
|
||||||
|
to a stub returning `KeychainUnavailable`; replace with Android
|
||||||
|
Keystore via JNI when sync auth ships on mobile.
|
||||||
|
- **Google Play Games (gpgs) integration.** Listed as a
|
||||||
|
Phase-Android target since Phase 1; now unblocked by the build
|
||||||
|
target.
|
||||||
|
- **Cosmetic `cargo apk build --lib` workaround.** Post-sign
|
||||||
|
panic doesn't affect the APK on disk but produces noisy stderr.
|
||||||
|
Either upstream a cargo-apk fix or document `--lib` as
|
||||||
|
canonical in the runbook.
|
||||||
|
|
||||||
|
### Visual-identity follow-ups (opened by v0.20.0's port)
|
||||||
|
|
||||||
|
- *Card-face / suit / card-back artwork regeneration — closed
|
||||||
|
2026-05-08 by the commit chain `5623368` → `dd101b3`.* The
|
||||||
|
Terminal spec called for dark `#1a1a1a` cards with light suit
|
||||||
|
pips (pink for hearts/diamonds, foreground gray for spades/
|
||||||
|
clubs). Closed across nine commits over two arcs:
|
||||||
|
- **Plan + tooling (`5623368`–`48b28d2`):** migration plan
|
||||||
|
doc, single-card PoC, full `card_face_generator` example
|
||||||
|
(52 faces + 5 backs into `assets/cards/`), and the
|
||||||
|
`card_face_svg_pin` integration test pinning rasteriser
|
||||||
|
output via FNV-1a so future `usvg`/`resvg` upgrades surface
|
||||||
|
as test failures rather than silent visual drift.
|
||||||
|
- **Lockstep step 4+5 (`e8bf9d7`):** PNGs + the 5 `card_plugin`
|
||||||
|
constants + signature shifts in one commit.
|
||||||
|
`CARD_FACE_COLOUR_RED_CBM` renamed to `RED_SUIT_COLOUR_CBM`
|
||||||
|
and repurposed from a face-tint to a suit-glyph swap (the
|
||||||
|
Terminal face is uniform `CARD_FACE_COLOUR` regardless of
|
||||||
|
CBM; CBM only swaps red suits to cyan in the glyph itself).
|
||||||
|
`face_colour` deleted, `text_colour` gained a `color_blind`
|
||||||
|
parameter.
|
||||||
|
- **Three follow-ups that surfaced during sign-off:**
|
||||||
|
`a14200a` regenerated the **default-theme SVGs** at
|
||||||
|
`solitaire_engine/assets/themes/default/*.svg` — those
|
||||||
|
`include_bytes!()`-embed into the binary and override
|
||||||
|
`assets/cards/*.png` at runtime, so the PNG migration alone
|
||||||
|
didn't change what production rendered. `8719f77`
|
||||||
|
regenerated `assets/backgrounds/bg_*.png` to flat Terminal
|
||||||
|
near-black (5 solid-colour PNGs via a new
|
||||||
|
`background_generator` example). `ae84dc1` cleared the
|
||||||
|
**top-bar overlap** at portrait/narrow window widths by
|
||||||
|
swapping the action-button row's hardcoded `font_size: 16.0`
|
||||||
|
to `TYPE_BODY` and stepping horizontal padding from
|
||||||
|
`VAL_SPACE_3` to `VAL_SPACE_2`.
|
||||||
|
- **Glyph-rendering fix (`af414b6`):** suit glyphs render as
|
||||||
|
inline SVG paths (not `<text>`) because the bundled
|
||||||
|
`FiraMono` doesn't carry usable U+2660-2666 at the
|
||||||
|
requested size — `usvg` was silently substituting tiny
|
||||||
|
"tofu" marks. Path-based rendering bypasses the font system
|
||||||
|
entirely; same bytes on every machine. The pin test
|
||||||
|
rebaselined cleanly via the empty-then-paste pattern.
|
||||||
|
- **Glyph-orientation tweak (`dd101b3`):** removed the 180°
|
||||||
|
rotation from the bottom-right large suit glyph at user
|
||||||
|
request — both glyphs now render in the same upright
|
||||||
|
orientation. `design-system.md` § Game Cards line 220
|
||||||
|
updated in lockstep to document the deliberate deviation
|
||||||
|
from the traditional inverted-corner-indicator convention.
|
||||||
|
- *Splash boot-loader scanline overlay — closed by `a27cf5a`.*
|
||||||
|
Runtime-generated 2 × 2 RGBA8 texture tiled via
|
||||||
|
`NodeImageMode::Tiled`; per-pixel alpha × tint alpha gives
|
||||||
|
multiplicative fade integration without new abstractions.
|
||||||
|
- *Splash cursor pulse — closed by `29136d8`.* Trailing 6 × 12 px
|
||||||
|
cyan Node, sine-pulsed, multiplied with the global splash fade
|
||||||
|
(the "multiply, don't override" pattern that resolves the
|
||||||
|
original `cacb19c` skip-rationale).
|
||||||
|
- **Replay-overlay enrichments beyond the scrub bar.** Banner-local
|
||||||
|
pieces of the mockup (`docs/ui-mockups/replay-overlay-mobile.html`)
|
||||||
|
all shipped: scrub bar (`c84d9f4`), `▌ replay` cursor-block label
|
||||||
|
(`6204db8`), `GAME #YYYY-DDD` caption (`54005d5`), `MOVE N/M`
|
||||||
|
chip restyle (`e080b49`). What's still open are the cross-plugin
|
||||||
|
/ data-layer pieces: a `MOVE N/M` chip *floating above the
|
||||||
|
focused card* during playback (would need to thread the cursor
|
||||||
|
through to the card layer — `update_progress_text` writes the
|
||||||
|
banner chip but the card-position lookup belongs in `card_plugin`).
|
||||||
|
The full mockup's screen-takeover treatment — mini-tableau
|
||||||
|
preview, playback controls, move-log scroll, WIN MOVE marker on
|
||||||
|
the scrub bar — is a multi-session redesign with
|
||||||
|
data-layer impact (move-log scroller; the WIN MOVE marker
|
||||||
|
needs a `win_move_index` field on `Replay` that doesn't yet
|
||||||
|
exist). Banner-overlay behaviour is intentionally preserved
|
||||||
|
for now.
|
||||||
|
- **Toast Warning / Error variants.** The `ToastVariant` enum
|
||||||
|
has slots for `Warning` (gold) and `Error` (pink) but no
|
||||||
|
in-engine event uses them yet. Wire when a warning- or error-
|
||||||
|
flavoured toast event materialises.
|
||||||
|
|
||||||
|
### Carried forward from v0.19.0
|
||||||
|
|
||||||
|
- **App icon round.** `Window::icon` not yet wired; no
|
||||||
|
`.icns` / `.ico` / Linux hicolor PNG hierarchy. The 11-size
|
||||||
|
icon export the v0.19 handoff referenced is *not* currently
|
||||||
|
in `artwork/` (current `artwork/` holds the reverted Rusty
|
||||||
|
Pixel card PNGs and is intentionally untracked); icon-export
|
||||||
|
needs to be re-run before this item can be picked up.
|
||||||
|
Half-day task once the PNGs are back in place. No cert
|
||||||
|
dependency.
|
||||||
|
|
||||||
|
### Other small candidates
|
||||||
|
|
||||||
|
- **Prev/Next selector chips spawn site.** v0.19.0's `9b065e5`
|
||||||
|
noted Prev/Next markers exist in `stats_plugin` but no spawn
|
||||||
|
site renders them today — the Shareable badge therefore lands
|
||||||
|
on the single-replay caption. If/when Prev/Next is plumbed,
|
||||||
|
the badge will need to follow.
|
||||||
|
- **Toast queue / immediate unification.** The two toast paths
|
||||||
|
(`spawn_queued_toast` for `InfoToastEvent` queue; `spawn_toast`
|
||||||
|
for fire-and-forget) now share visual treatment but remain
|
||||||
|
separate functions because they serve different temporal
|
||||||
|
needs (sequential vs. parallel). If overlap becomes a UX
|
||||||
|
issue, merge into one queue with priority lanes.
|
||||||
|
|
||||||
|
### Process notes
|
||||||
|
|
||||||
|
- **The desktop-adaptation spec is the canonical reference for
|
||||||
|
geometry decisions** when porting any future plugin. Read
|
||||||
|
`docs/ui-mockups/desktop-adaptation.md` first; apply the
|
||||||
|
universal rules to every surface; consult the per-screen
|
||||||
|
table for the priority surfaces. The 9 missing-plugin screens
|
||||||
|
(splash now ported; eight remaining) inherit the universal
|
||||||
|
rules without dedicated guidance.
|
||||||
|
- **Stitch `generate_variants` is unreliable for layout-only
|
||||||
|
adaptation prompts** as of 2026-05-07. The first call timed
|
||||||
|
out and no variant ever landed in `list_screens`. If a future
|
||||||
|
session wants visual desktop mockups, prefer
|
||||||
|
`generate_screen_from_text` with a fresh narrow prompt per
|
||||||
|
screen rather than `generate_variants` against existing
|
||||||
|
mobile screens.
|
||||||
|
- **Token-port pattern.** v0.20.0's chrome-migration commits
|
||||||
|
set a reusable shape for "centralised design system applied
|
||||||
|
across N plugins":
|
||||||
|
1. Constants module (`ui_theme.rs`) is the source of truth.
|
||||||
|
2. Const sites that can't call `Alpha::with_alpha` (not yet
|
||||||
|
`const` on stable) use a literal RGB matching the token,
|
||||||
|
with a unit test pinning the RGB to the token (e.g.
|
||||||
|
`MARKER_VALID`, `HINT_PILE_HIGHLIGHT_COLOUR`,
|
||||||
|
`RIGHT_CLICK_HIGHLIGHT_COLOUR`).
|
||||||
|
3. Cross-plugin duplication (e.g. `MARKER_DEFAULT` ↔
|
||||||
|
`PILE_MARKER_DEFAULT_COLOUR`) collapses to a single
|
||||||
|
promoted const re-exported from one plugin and imported
|
||||||
|
by the other — replaces "kept in sync" doc comments with a
|
||||||
|
compile-time invariant.
|
||||||
|
4. Domain colours (suit pips, card faces, lerp helpers) stay
|
||||||
|
as literals with a comment naming the rationale; only UI
|
||||||
|
chrome routes through tokens.
|
||||||
|
- **`SplashFadable` scaffolding pattern** (introduced in
|
||||||
|
`cacb19c`). Any future overlay that needs to fade `N >> 3`
|
||||||
|
elements together should follow the same shape: one tiny
|
||||||
|
marker carrying the full-alpha base colour, one global query
|
||||||
|
that lerps every marker's alpha each frame, no per-element
|
||||||
|
query plumbing. Cleanly outscales the `Without<X>, Without<Y>`
|
||||||
|
query exclusion pattern that the old splash was hitting at
|
||||||
|
three siblings.
|
||||||
|
|
||||||
|
### Canonical remote
|
||||||
|
|
||||||
|
`github.com/funman300/Rusty_Solitaire` is the canonical repo.
|
||||||
|
Always push there. **Local master has unpushed post-cut commits**
|
||||||
|
— run `git log --oneline origin/master..HEAD` for the live list;
|
||||||
|
`git push` is the next durability step (or roll the post-cut
|
||||||
|
commits into v0.20.1).
|
||||||
|
|
||||||
|
### Design direction (Terminal — base16-eighties)
|
||||||
|
|
||||||
|
- **Tone:** retro-terminal / synthwave — flat depth (no box-shadows),
|
||||||
|
monospaced-forward typography (JetBrains Mono / FiraMono), tight
|
||||||
|
16 px edge margins, 8 px card radius.
|
||||||
|
- **Palette:** near-black surface ramp (`#151515` / `#202020` /
|
||||||
|
`#2a2a2a` / `#353535`), cyan primary CTA (`#6fc2ef`), lime
|
||||||
|
success (`#acc267`), gold warning (`#ddb26f`), pink error /
|
||||||
|
suit-red (`#fb9fb1`), lavender celebration (`#e1a3ee`), teal
|
||||||
|
info (`#12cfc0`).
|
||||||
|
- **Two-color suits.** Red = `#fb9fb1`, black = `#d0d0d0`.
|
||||||
|
Outlined glyphs for diamonds & clubs are *always on*; the
|
||||||
|
Settings "color-blind mode" toggle only swaps red → cyan.
|
||||||
|
|
||||||
|
## Resume prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||||
|
Working directory: <Rusty_Solitaire clone path on this machine>.
|
||||||
|
Branch: master. v0.20.0 is tagged at 41a009a; the post-cut work
|
||||||
|
through dd101b3 is pushed to origin (Options B, C, D all closed).
|
||||||
|
Run `git log --oneline 41a009a..HEAD` to see what landed since the
|
||||||
|
tag — substantives: desktop-adaptation spec, splash boot-screen
|
||||||
|
port, replay-overlay banner enrichments, and the full card-face
|
||||||
|
artwork arc (52 faces + 5 backs as Terminal SVG-rasterised PNGs,
|
||||||
|
default-theme SVGs in lockstep, table backgrounds flattened,
|
||||||
|
top-bar layout fix, glyph orientation upright).
|
||||||
|
|
||||||
|
State: HEAD locally — see `git rev-parse HEAD`. Working tree is
|
||||||
|
clean. All workspace tests pass (~1180+; check with
|
||||||
|
`cargo test --workspace`), clippy clean.
|
||||||
|
|
||||||
|
READ FIRST (in order, before doing anything):
|
||||||
|
1. SESSION_HANDOFF.md — this file
|
||||||
|
2. CHANGELOG.md — [0.20.0] section is the most recent cut
|
||||||
|
3. CLAUDE.md — unified-3.0 rule set
|
||||||
|
4. CLAUDE_SPEC.md — formal architecture spec
|
||||||
|
5. ARCHITECTURE.md — crate responsibilities + data flow
|
||||||
|
6. docs/ui-mockups/ — design system + 24-mockup library +
|
||||||
|
desktop-adaptation.md (the rules-based
|
||||||
|
companion to the mockups; read this
|
||||||
|
before any plugin port)
|
||||||
|
7. docs/android/* — Android setup + build runbook
|
||||||
|
8. ~/.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. Push the post-cut commits to origin. Either as-is on master
|
||||||
|
or rolled into a v0.20.1 cut (CHANGELOG entry + tag).
|
||||||
|
Mechanical, but local master diverges from origin until done.
|
||||||
|
B. *Closed by `29136d8` + `a27cf5a`.* Both splash polish
|
||||||
|
pieces shipped (cursor pulse + scanline overlay). No further
|
||||||
|
splash work pending unless a new mockup detail surfaces.
|
||||||
|
C. *Closed by `54005d5` + `e080b49`.* Banner-local replay-overlay
|
||||||
|
pieces all shipped (scrub bar, ▌ label, GAME caption, MOVE
|
||||||
|
chip). Remaining are cross-plugin (floating MOVE chip above
|
||||||
|
the focused card — needs cursor → card-position plumbing) or
|
||||||
|
multi-session (full screen-takeover redesign — move-log
|
||||||
|
scroll, mini tableau, WIN MOVE marker, data-layer impact).
|
||||||
|
Either belongs in its own decision tree the next time replay
|
||||||
|
work surfaces.
|
||||||
|
D. *Closed 2026-05-08 by `5623368`…`dd101b3`.* The full
|
||||||
|
card-face / suit / card-back / default-theme / table-
|
||||||
|
background / top-bar / glyph-orientation arc landed across
|
||||||
|
nine commits. Terminal cards rendering on every face (dark
|
||||||
|
`#1a1a1a` background, pink/gray suit glyphs as inline SVG
|
||||||
|
paths, scanline-pattern cyan-accent backs); both rendering
|
||||||
|
paths (`assets/cards/*.png` and the bundled-default theme
|
||||||
|
SVGs at `solitaire_engine/assets/themes/default/*.svg`) in
|
||||||
|
lockstep; pin test (`card_face_svg_pin`) guards against
|
||||||
|
future rasteriser drift. Visual-identity arc effectively
|
||||||
|
complete — only the toast warning/error variant slots
|
||||||
|
remain wired-but-unused.
|
||||||
|
E. App icon round — re-run artwork/Icon Export.html (the
|
||||||
|
export PNGs are not currently in `artwork/`), then wire
|
||||||
|
Window::icon + generate .icns / .ico. Half-day task. No
|
||||||
|
cert dependency.
|
||||||
|
F. APK launch verification on AVD / device + the JNI bridges
|
||||||
|
it would shake out (ClipboardManager, Keystore).
|
||||||
|
|
||||||
|
WORKFLOW NOTES:
|
||||||
|
- Use the system git config (already correct).
|
||||||
|
- 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) — gh auth setup-git wired on
|
||||||
|
primary dev box; verify on laptop before first push.
|
||||||
|
|
||||||
|
OPEN AT THE START: ask which of A–F. Don't pick unilaterally.
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 469 B |
|
After Width: | Height: | Size: 469 B |
|
After Width: | Height: | Size: 469 B |
|
After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |