Compare commits
200 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e107f5e218 | |||
| 463b7465ed | |||
| 92a5ebb15e | |||
| 89a21c0587 | |||
| 304cb050a7 | |||
| fcc7337c97 | |||
| 16ce2b88d2 | |||
| b9aa2620b8 | |||
| 47f02a60ae | |||
| a5c3188686 | |||
| 6a289b7b50 | |||
| bee712c5ab | |||
| 0db5e9dac4 | |||
| 681a54d9bb | |||
| 7894559ca7 | |||
| ab803c07af | |||
| e43b329fc1 | |||
| 7c07f71f02 | |||
| c1329bbb21 | |||
| 4303ef3f5b | |||
| 4df962ee07 | |||
| f281425b45 | |||
| 2c822ba2d7 | |||
| 7ddf2733c9 | |||
| 585570559c | |||
| 45436d0eda | |||
| 2062bd06f3 | |||
| 0cb15872b1 | |||
| 395a322adc | |||
| 5199a5e499 | |||
| 16242e6d77 | |||
| 202a64db45 | |||
| c0415eb0ee | |||
| a449f60bc5 | |||
| ad5f613277 | |||
| c50eaf81f7 | |||
| b44d2777ec | |||
| 52407e7256 | |||
| da3e5423dc | |||
| a1864271de | |||
| f63db769ae | |||
| 4437a1aaf9 | |||
| e7345aed6c | |||
| 140251beae | |||
| d6f32d3154 | |||
| 8fdc41f36f | |||
| 2e25476d0a | |||
| d3cb1a51d4 | |||
| c8358f4275 | |||
| a2432dfe7a | |||
| 511550232c | |||
| e5c4f51a6e | |||
| 23902cdc44 | |||
| 3cc8eacafa | |||
| 90e24d9711 | |||
| decbe0bbd9 | |||
| 1873b3f9be | |||
| d11d97e677 | |||
| d322abf67b | |||
| c9e4c0b4cd | |||
| fe68861e10 | |||
| c33b39cf11 | |||
| 23ff62c397 | |||
| 0b2ffca016 | |||
| fbe48acef6 | |||
| cd79877933 | |||
| 52befa6199 | |||
| e63046700c | |||
| ab857bbb6e | |||
| 886e0cf8a1 | |||
| 3d92a91e3b | |||
| 9113cdb483 | |||
| c153363626 | |||
| 93b67f1d0b | |||
| 279e23d0af | |||
| 12fba2157a | |||
| f23df3b805 | |||
| 68d50b5021 | |||
| ec804d54c6 | |||
| d87761d451 | |||
| 2fb2d638bf | |||
| c9af1ead22 | |||
| ed152e2d8f | |||
| 279a834f9d | |||
| daa655a0af | |||
| 4d48cad4e3 | |||
| dd970215cc | |||
| ddb65403c2 | |||
| 62b61cc786 | |||
| 31139ae455 | |||
| 07e035771c | |||
| c5787c6953 | |||
| 716a025352 | |||
| 3eb3a26789 | |||
| 0c1cc40266 | |||
| 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 |
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
libxkbcommon-dev
|
libxkbcommon-dev
|
||||||
|
|
||||||
- name: Cache cargo registry and build artifacts
|
- name: Cache cargo registry and build artifacts
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
needs: test
|
needs: test
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
libxkbcommon-dev
|
libxkbcommon-dev
|
||||||
|
|
||||||
- name: Cache cargo registry and build artifacts
|
- name: Cache cargo registry and build artifacts
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Triggered by pushing a version tag, e.g. `git tag v0.22.0 && git push origin v0.22.0`.
|
||||||
|
# Builds a Linux x86_64 tarball and a signed Android APK, then publishes
|
||||||
|
# both as assets on a GitHub Release. Obtainium can track this repo's
|
||||||
|
# releases and download the APK automatically.
|
||||||
|
#
|
||||||
|
# Required repository secrets (Settings → Secrets and variables → Actions):
|
||||||
|
# ANDROID_KEYSTORE_BASE64 base64-encoded .jks file (see README for gen command)
|
||||||
|
# ANDROID_KEYSTORE_PASSWORD password used with -storepass when creating the keystore
|
||||||
|
# ANDROID_KEY_ALIAS alias used with -alias when creating the keystore
|
||||||
|
# ANDROID_KEY_PASSWORD password used with -keypass when creating the keystore
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # gh release create needs write access
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUSTFLAGS: "-D warnings"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 1: Linux x86_64 binary + assets tarball
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
name: Build · Linux x86_64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install system deps
|
||||||
|
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 + build artifacts
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: linux-release-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: linux-release-
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release -p solitaire_app
|
||||||
|
|
||||||
|
- name: Package tarball
|
||||||
|
run: |
|
||||||
|
mkdir solitaire-quest
|
||||||
|
cp target/release/solitaire_app solitaire-quest/
|
||||||
|
cp -r assets solitaire-quest/
|
||||||
|
tar -czf solitaire-quest-linux-x86_64.tar.gz solitaire-quest
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
path: solitaire-quest-linux-x86_64.tar.gz
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 2: Android APK (multi-arch) — release-built and signed via cargo-apk
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
build-android:
|
||||||
|
name: Build · Android APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Install Rust stable + Android targets
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
|
||||||
|
|
||||||
|
- name: Expose NDK root to cargo-apk
|
||||||
|
# ANDROID_NDK_LATEST_HOME is set by the GitHub-hosted runner.
|
||||||
|
# cargo-apk reads ANDROID_NDK_ROOT; write it to GITHUB_ENV so
|
||||||
|
# all subsequent steps in this job inherit it.
|
||||||
|
run: echo "ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache cargo registry + cargo-apk binary + build artifacts
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
~/.cargo/bin
|
||||||
|
target
|
||||||
|
key: android-release-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: android-release-
|
||||||
|
|
||||||
|
- name: Install cargo-apk
|
||||||
|
# --locked: use the dependency versions cargo-apk was tested with.
|
||||||
|
# cargo install is a no-op when the cached binary is already current.
|
||||||
|
run: cargo install --locked cargo-apk
|
||||||
|
|
||||||
|
- name: Inject release signing config
|
||||||
|
# cargo-apk --release requires [package.metadata.android.signing.release]
|
||||||
|
# in solitaire_app/Cargo.toml. Appended at CI time so secrets never
|
||||||
|
# live in the repo. printf keeps every line inside the YAML run block,
|
||||||
|
# avoiding the YAML parse error a heredoc with column-0 content causes.
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > release.keystore
|
||||||
|
{
|
||||||
|
printf '\n[package.metadata.android.signing.release]\n'
|
||||||
|
printf 'path = "%s"\n' "${GITHUB_WORKSPACE}/release.keystore"
|
||||||
|
printf 'keystore_password = "%s"\n' "$ANDROID_KEYSTORE_PASSWORD"
|
||||||
|
printf 'key_alias = "%s"\n' "$ANDROID_KEY_ALIAS"
|
||||||
|
printf 'key_password = "%s"\n' "$ANDROID_KEY_PASSWORD"
|
||||||
|
} >> solitaire_app/Cargo.toml
|
||||||
|
|
||||||
|
- name: Build and sign APK (release profile)
|
||||||
|
# `--lib` scopes cargo-apk to the cdylib target only.
|
||||||
|
# Without it, cargo-apk panics post-sign with
|
||||||
|
# "Bin is not compatible with Cdylib" (cargo-subcommand
|
||||||
|
# artifact iteration walks the bin target after the
|
||||||
|
# cdylib APK is already produced). See SESSION_HANDOFF.md
|
||||||
|
# "Cosmetic cargo apk build --lib workaround."
|
||||||
|
run: cargo apk build -p solitaire_app --lib --release
|
||||||
|
|
||||||
|
- name: Stage APK for upload
|
||||||
|
run: |
|
||||||
|
cp target/release/apk/solitaire-quest.apk \
|
||||||
|
"solitaire-quest-${{ github.ref_name }}.apk"
|
||||||
|
rm release.keystore
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: android
|
||||||
|
path: solitaire-quest-${{ github.ref_name }}.apk
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Job 3: Create the GitHub Release once both builds succeed
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
release:
|
||||||
|
name: Publish GitHub Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-linux, build-android]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
name: android
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh release create "${{ github.ref_name }}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--title "Solitaire Quest ${{ github.ref_name }}" \
|
||||||
|
--generate-notes \
|
||||||
|
"solitaire-quest-linux-x86_64.tar.gz" \
|
||||||
|
"solitaire-quest-${{ github.ref_name }}.apk"
|
||||||
@@ -7,3 +7,11 @@
|
|||||||
*.tmp
|
*.tmp
|
||||||
data/
|
data/
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# IDE project files
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Android signing keystores — never commit
|
||||||
|
*.jks
|
||||||
|
*.jks.bak
|
||||||
|
*.keystore
|
||||||
|
|||||||
@@ -47,11 +47,10 @@ Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, tar
|
|||||||
### 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.
|
||||||
- **UI-first interaction.** Every player-triggered action — new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, etc. — must be reachable from a visible UI control. Keyboard shortcuts exist only as optional accelerators for power users; they are never the sole entry point. A player using only mouse or touch must be able to perform every action. New gameplay features ship with the UI control alongside the system that backs it.
|
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -716,11 +715,14 @@ 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Progression System
|
## 12. Progression System
|
||||||
|
|||||||
@@ -1,114 +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_app/ # Thin binary entry point
|
For full system design details:
|
||||||
assets/ # Source assets — embedded at compile time via include_bytes!()
|
→ `ARCHITECTURE.md` (authoritative source of truth)
|
||||||
|
|
||||||
|
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>`.
|
|
||||||
- Audio assets are embedded at compile time using `include_bytes!()` in `audio_plugin.rs`.
|
```text id="commit_fmt"
|
||||||
- Card faces (52 PNGs in `assets/cards/faces/`), card backs (`assets/cards/backs/back_N.png`), board backgrounds (`assets/backgrounds/bg_N.png`), and the UI font (`assets/fonts/main.ttf`) are loaded at runtime via `AssetServer::load()` and stored as `Handle<Image>`/`Handle<Font>` in the `CardImageSet`, `BackgroundImageSet`, and `FontResource` resources. The `assets/` directory must ship alongside the binary.
|
type(scope): description
|
||||||
- Asset-loading systems take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` (tests). When `CardImageSet` is absent, `card_plugin` falls back to a `Text2d` rank+suit overlay; when `BackgroundImageSet` is absent, the board falls back to a solid colour.
|
```
|
||||||
- Atomic file writes only: write to `filename.json.tmp`, then `rename()`.
|
|
||||||
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
|
Examples:
|
||||||
- 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.
|
* feat(core): add draw-three rules
|
||||||
- `cargo clippy --workspace -- -D warnings` must pass clean after every change.
|
* fix(engine): correct drag z-order
|
||||||
- `cargo test --workspace` must pass after every change.
|
* 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`
|
||||||
- **UI-first.** Every player-triggered action (new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, switch mode, etc.) must be reachable from a visible UI control. Keyboard shortcuts are optional accelerators — never the sole entry point. New gameplay features ship with the UI control alongside the system that backs it; do not merge a feature that is keyboard-only.
|
* 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
|
||||||
@@ -30,13 +30,66 @@ dirs = "6"
|
|||||||
keyring = "4"
|
keyring = "4"
|
||||||
keyring-core = "1"
|
keyring-core = "1"
|
||||||
reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
|
reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
|
||||||
|
arboard = { version = "3", default-features = false }
|
||||||
|
jni = { version = "0.21", 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.18"
|
# Bevy with `default-features = false` to avoid the unused
|
||||||
|
# `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"
|
kira = "0.12"
|
||||||
|
|
||||||
# SVG rasterisation pipeline for the runtime card-theme system.
|
# SVG rasterisation pipeline for the runtime card-theme system.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ optional self-hosted sync so your stats follow you across machines.
|
|||||||
move within picker rows, Enter activates; works across every modal and
|
move within picker rows, Enter activates; works across every modal and
|
||||||
the HUD action bar
|
the HUD action bar
|
||||||
- **Progression** — XP, levels, unlockable card backs and backgrounds
|
- **Progression** — XP, levels, unlockable card backs and backgrounds
|
||||||
- **18 Achievements** — including secret ones
|
- **19 Achievements** — including secret ones
|
||||||
- **Daily Challenge** — server-seeded so every player worldwide gets the
|
- **Daily Challenge** — server-seeded so every player worldwide gets the
|
||||||
same deal
|
same deal
|
||||||
- **Leaderboard** — opt-in, powered by your own self-hosted server
|
- **Leaderboard** — opt-in, powered by your own self-hosted server
|
||||||
|
|||||||
@@ -1,140 +1,338 @@
|
|||||||
# Solitaire Quest — Session Handoff
|
# Solitaire Quest — Session Handoff
|
||||||
|
|
||||||
**Last updated:** 2026-05-02 (session 9, post-v0.14.0 release prep) — v0.14.0 cut. The Quat bug fixes, the rest of the v0.13.0 candidate list, and the entire replay → upload → web-viewer pipeline are all bundled in this release. Direction now opens for the next round.
|
**Last updated:** 2026-05-08 — **v0.21.8 tagged at `c50eaf8`**;
|
||||||
|
nine post-cut commits on master. Push pending.
|
||||||
|
|
||||||
|
v0.21.8 closes the last optional polish items in the B-2
|
||||||
|
replay screen-takeover arc: **notch-label centering** (middle
|
||||||
|
three scrub-bar labels now centred on their notch ticks via the
|
||||||
|
CSS `translateX(-50%)` pattern for Bevy 0.18 UI) and **WIN
|
||||||
|
MOVE HC legibility** (lime stays lime under HC mode via the
|
||||||
|
extended `HighContrastBackground::with_hc` constructor and a
|
||||||
|
new `STATE_SUCCESS_HC` brighter-lime constant). The replay
|
||||||
|
overlay arc is now fully closed with no known open items.
|
||||||
|
|
||||||
|
Full v0.21.8 detail lives in `CHANGELOG.md` § [0.21.8]. This
|
||||||
|
file from here on focuses on what's *open* post-cut and how to
|
||||||
|
resume.
|
||||||
|
|
||||||
## Status at pause
|
## Status at pause
|
||||||
|
|
||||||
- **HEAD on origin:** v0.14.0's tag commit (CHANGELOG + handoff refresh).
|
- **HEAD locally:** `f281425` (Android Keystore JNI).
|
||||||
- **Working tree:** clean apart from untracked `CARD_PLAN.md` (intentional).
|
Docs ride on top; push pending.
|
||||||
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings` clean.
|
- **HEAD on origin:** `395a322` (double-tap commit — last pushed).
|
||||||
- **Tests:** **1134 passed / 0 failed** across the workspace.
|
- **Working tree:** clean (docs uncommitted). No WIP outstanding.
|
||||||
- **Tags on origin:** `v0.9.0`, `v0.10.0`, `v0.11.0`, `v0.12.0`, `v0.13.0`, `v0.14.0`.
|
- **`artwork/` directory:** still untracked. Intentional.
|
||||||
|
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
|
||||||
|
clean.
|
||||||
|
- **Tests:** **1292 passing / 0 failing** across the workspace.
|
||||||
|
- **Tags on origin:** `v0.9.0` through `v0.21.8`.
|
||||||
|
- **Android:** APK verified booting on Pixel_7 AVD (Android 14,
|
||||||
|
x86_64). All desktop-only systems (handle_fullscreen) now gated.
|
||||||
|
See Phase Android punch list for remaining work.
|
||||||
|
|
||||||
## Where we are
|
## Since the v0.21.8 cut
|
||||||
|
|
||||||
v0.14.0 is the largest release since the card-theme system. Three threads land together:
|
Seven commits since the v0.21.8 tag:
|
||||||
|
- `a449f60` — Stats Prev/Next selector spawn site
|
||||||
|
- `202a64d` — Android launch fixes (android_main, resize_constraints,
|
||||||
|
apply_smart_default_window_size) — **closes APK launch verification**
|
||||||
|
- `16242e6` — Ignore .idea/ IDE files
|
||||||
|
- `395a322` — double-tap auto-move for touch input
|
||||||
|
- `0cb1587` — Play-by-Seed dialog + HomeMode card
|
||||||
|
- `2062bd0` — 75 new challenge seeds + gen_seeds binary
|
||||||
|
- `45436d0` — gate handle_fullscreen to non-Android
|
||||||
|
- `2c822ba` — JNI clipboard bridge for Android Stats share-link
|
||||||
|
- `f281425` — Android Keystore AES-GCM token storage via JNI
|
||||||
|
|
||||||
1. **The remaining v0.13.0-era UX candidates** — theme thumbnails, daily-challenge calendar, Time Attack auto-save, per-mode bests, time-bonus multiplier slider.
|
CHANGELOG + SESSION_HANDOFF docs ride on top; push pending.
|
||||||
2. **Quat smoke-test bug fixes** — multi-card move validation, softlock detection, deal-tween information leak.
|
|
||||||
3. **The replay pipeline** — record on win, persist to disk, upload to server, view in browser via a new `solitaire_wasm` crate. The biggest single feature since the card-theme system.
|
|
||||||
|
|
||||||
The card-flight web animations and replay E2E test coverage close out the pipeline.
|
Open next-step menu:
|
||||||
|
1. **Phase 8 (sync)** — the biggest open arc. Local storage
|
||||||
### Design direction (unchanged)
|
scaffolding, self-hosted Axum server, GPGS stub.
|
||||||
|
2. **Android follow-ups** — JNI ClipboardManager, Android Keystore,
|
||||||
- **Tone:** Balatro — chunky readable type, theatrical hierarchy, satisfying micro-interactions.
|
GPGS. Launch verification and double-tap both closed; these
|
||||||
- **Palette:** Midnight Purple base + Balatro yellow primary + warm magenta secondary.
|
are the remaining Phase Android items.
|
||||||
- See `~/.claude/projects/-home-manage-Rusty-Solitare/memory/project_ux_overhaul_2026-04.md` (machine-local).
|
3. **Move Log auto-scroll** — only relevant if the panel
|
||||||
|
row count grows beyond the current 5-row fixed window.
|
||||||
### Canonical remote
|
|
||||||
|
|
||||||
`github.com/funman300/Rusty_Solitaire` is the canonical repo. Always push there.
|
|
||||||
|
|
||||||
## Session 8 + 9 (shipped 2026-05-02) — v0.14.0
|
|
||||||
|
|
||||||
### v0.13.0-era UX candidates (had landed but missed v0.13.0's tag)
|
|
||||||
|
|
||||||
| Area | Commit | What landed |
|
|
||||||
|---|---|---|
|
|
||||||
| Theme thumbnails | `ba527de` | Each Settings → Cosmetic theme chip renders an Ace + back preview pair via `rasterize_svg`. Cached per theme. Missing-SVG themes show a transparent placeholder rather than crashing. |
|
|
||||||
| Daily-challenge calendar | `1a10476` | 14-dot horizontal calendar in the Profile modal. Today is ringed, completed days fill `STATE_SUCCESS`, missed days fill `BG_ELEVATED`. Caption: "Current streak: N · Longest: M". `PlayerProgress` gains `daily_challenge_history` (capped at 365) and `daily_challenge_longest_streak`. |
|
|
||||||
| Time Attack auto-save | `0001432` | New sibling `time_attack_session.json` next to `game_state.json`. Atomic .tmp + rename. 30 s auto-save while active + on `AppExit`. Sessions whose 10-min window expired in real time while the app was closed are discarded on load. |
|
|
||||||
| Per-mode bests | `3984231` | StatsSnapshot gains six `#[serde(default)]` fields (Classic / Zen / Challenge × best_score + fastest_win_seconds). Stats screen renders a "Per-mode bests" section. Lifetime totals continue to roll all modes together. |
|
|
||||||
| Time-bonus slider | `89c51ab` | Settings → Gameplay slider 0.0–2.0, default 1.0, "Off" at zero. Multiplies the time-bonus shown in the win modal. Cosmetic only — does NOT affect achievement unlock thresholds. |
|
|
||||||
|
|
||||||
### Quat smoke-test bug fixes
|
|
||||||
|
|
||||||
| Area | Commit | What landed |
|
|
||||||
|---|---|---|
|
|
||||||
| Move validation (#1) | `f1aeb24` | `solitaire_core::rules::is_valid_tableau_sequence(&[Card]) -> bool` checks every adjacent pair in a moved stack descends one rank with alternating colour. Wired into `move_cards`. Closes the bug where any multi-card lift could be dropped as long as the bottom landed legally. |
|
|
||||||
| Deal-tween leak (#4) | `3eabc14` | New-game snaps every card sprite to the stock pile position before writing `StateChangedEvent`, so all 52 cards animate from a single deck point during the deal. Previously sprites started from previous-game positions, briefly revealing the prior deal. |
|
|
||||||
| Softlock detection (#2) | `2716472` | `has_legal_moves` rewritten: walks every potential move source (every stock card, every waste card, the face-up top of every tableau column) against every foundation and every tableau. Previous heuristic returned `true` whenever stock had cards, hiding genuine softlocks. `GameOverScreen` now actually fires for true softlocks. |
|
|
||||||
| End-game screen (#3) | — | Resolved as downstream of #2. The pre-existing `GameOverScreen` and `WinSummaryOverlay` already cover the close-out paths; the softlock screen just never spawned because the old `has_legal_moves` lied. |
|
|
||||||
|
|
||||||
### Replay pipeline (the major feature)
|
|
||||||
|
|
||||||
| Area | Commit | What landed |
|
|
||||||
|---|---|---|
|
|
||||||
| Replay storage | `42535f5` | `solitaire_data::replay::Replay` (seed + draw_mode + mode + score + time + recorded date + ordered move list) and atomic save/load helpers under `<data_dir>/latest_replay.json`. Schema v1; `load` returns None for any other version. |
|
|
||||||
| Engine recording | `57d1c58` | `RecordingReplay` resource + `ReplayPath` settings. Every successful `MoveRequestEvent` / `DrawRequestEvent` appends to recording; `GameWonEvent` freezes the recording into a `Replay` and persists. Undo intentionally not recorded. New game clears the recording. |
|
|
||||||
| Stats button | `d9f36bf` | Stats overlay surfaces a "Latest win:" caption + "Watch replay" button. Loads from disk via `LatestReplayResource`. (Full in-engine playback deferred — button currently fires an `InfoToastEvent` describing the replay.) |
|
|
||||||
| Server upload + fetch | `93182fa` | `POST /api/replays` accepts a `Replay` JSON; `GET /api/replays/:id` returns it. JWT-gated. SQL migration for the new `replays` table. |
|
|
||||||
| Engine sync | `23c9704` | Engine uploads winning replays automatically when the player has cloud sync configured. Re-uses the existing JWT/refresh-token flow. |
|
|
||||||
| WASM crate | `5bed43e` | New workspace member `solitaire_wasm` compiles replay-relevant `solitaire_core` types to WebAssembly so a browser can re-execute a replay client-side. `wasm-bindgen` glue. |
|
|
||||||
| Web viewer | `07b8ecd` | `GET /replays/:id` returns HTML + CSS + the wasm bundle. Browser fetches the replay JSON, rasterises a deal from the seed, and animates the recorded moves. |
|
|
||||||
| E2E coverage | `3081505` | Server tests covering the full upload → fetch round-trip via `axum::test`. |
|
|
||||||
| Web flight anim | `1fcd032` | Card-flight tweens on the web side so the browser viewer reads as a real game replay rather than a static dump. |
|
|
||||||
|
|
||||||
## Open punch list
|
## Open punch list
|
||||||
|
|
||||||
### Release prep
|
### Phase Android (build + persistence shipped; runtime gaps remain)
|
||||||
1. **Smoke-test on the alex machine** after pulling — confirm Quat's three bug fixes hold up in real gameplay, and try the new replay button + web viewer end-to-end.
|
|
||||||
2. **Desktop packaging** per `ARCHITECTURE.md §17`. The Arch PKGBUILD exists in `/home/manage/solitaire-quest-pkgbuild/` (separate repo). Pending: app icon, macOS `.icns` + notarisation cert, Windows `.ico` + Authenticode cert, AppImage recipe.
|
|
||||||
|
|
||||||
### UX iteration (next-round candidates)
|
- *APK launch verification — closed 2026-05-08 by `202a64d`.*
|
||||||
|
Three fixes shipped: `android_main` export (missing NativeActivity
|
||||||
|
entry point), `resize_constraints` gated to non-Android (max=0
|
||||||
|
panic), `apply_smart_default_window_size` gated to non-Android
|
||||||
|
(clamp panic on zero-dimension window event). Verified booting on
|
||||||
|
Pixel_7 AVD (Android 14, x86_64, SwiftShader Vulkan), 2+ min
|
||||||
|
runtime without crash. B0004 ECS hierarchy warnings remain
|
||||||
|
(non-fatal; entity parent/child component mismatch); investigate
|
||||||
|
if they surface gameplay bugs.
|
||||||
|
- *Double-tap auto-move — closed 2026-05-08 by `395a322`.*
|
||||||
|
`handle_double_tap` fires `MoveRequestEvent` on two rapid
|
||||||
|
`TouchPhase::Ended` events within 0.5 s. Prefers foundation;
|
||||||
|
falls back to tableau stack move. Fires `MoveRejectedEvent` when
|
||||||
|
no legal destination exists. System runs before `touch_end_drag`
|
||||||
|
in the chain so drag state is readable.
|
||||||
|
- *F11 fullscreen gate — closed 2026-05-08 by `45436d0`.*
|
||||||
|
`handle_fullscreen` and its `MonitorSelection`/`WindowMode`
|
||||||
|
imports are `#[cfg(not(target_os = "android"))]`-gated. The
|
||||||
|
`add_systems` call is a separate statement (not mid-chain).
|
||||||
|
- *JNI ClipboardManager bridge — closed 2026-05-08 by `2c822ba`.*
|
||||||
|
`android_clipboard::set_text(url)` calls `ClipboardManager` via
|
||||||
|
JNI. Stats share-link button now writes to the clipboard with a
|
||||||
|
"Copied: {url}" toast; falls back to "Share link: {url}" on JNI
|
||||||
|
error. Requires AVD functional test (see verification steps in
|
||||||
|
the approved plan).
|
||||||
|
- *Android Keystore for credentials — closed 2026-05-08 by `f281425`.*
|
||||||
|
`android_keystore` module: AES-256/GCM/NoPadding device-bound key,
|
||||||
|
tokens serialised to JSON and stored atomically at
|
||||||
|
`{data_dir}/auth_tokens.bin` as `[12-byte IV][ciphertext+tag]`.
|
||||||
|
`auth_tokens.rs` Android stubs now delegate to it. Key
|
||||||
|
invalidation (biometric reset) → `TokenError::KeychainUnavailable`.
|
||||||
|
Requires AVD functional test before Phase 8 sync goes live on
|
||||||
|
Android.
|
||||||
|
- **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.
|
||||||
|
|
||||||
- **Solver-at-deal toggle** (Quat investigation #1, still deferred): add a Settings → Gameplay toggle "Winnable deals only" rather than baking solver-only into every deal. Lightest middle ground.
|
### Visual-identity follow-ups (post-v0.21.0)
|
||||||
- **Disable Bevy's default audio feature** (Quat investigation #2, still deferred): one-line `default-features = false` swap on the workspace `bevy =` line, re-enable explicitly the features the engine uses (`render`, `bevy_winit`, `2d`, `bevy_window`, `png`, `bevy_text`, `bevy_ui`, `bevy_log`, `bevy_asset`, `default_font`, `bevy_state`). Drops ~50 transitive crates including the rodio + symphonia stack the project doesn't use (kira handles audio).
|
|
||||||
- **In-engine replay playback** — promote the "Watch replay" button from a stub toast to a real playback overlay that re-runs the recorded moves with `CardAnimation` tweens. The wasm crate already proves the playback math; the in-engine version reuses the same execute logic against the live game state.
|
|
||||||
- **Per-replay history** — currently single-slot at `latest_replay.json`. A "best replay per mode" bucket or a recent-N rolling list would let players revisit notable wins.
|
|
||||||
- **Solver-driven hint system** — extend the existing hint toggle so a deal-time solver provides higher-quality hints (currently a heuristic). Requires the solver from the toggle work above.
|
|
||||||
- **Achievement: "won via replay path"** — track when a player wins a deal whose previously-saved replay also won the same deal. Mostly fun; trivial scope.
|
|
||||||
|
|
||||||
## Card-theme system (CARD_PLAN.md, fully shipped)
|
The visual-identity arc is effectively complete: token system,
|
||||||
|
chrome migration, splash boot screen, replay-overlay banner,
|
||||||
|
card-face artwork (both rendering paths), and the `ACCENT_PRIMARY`
|
||||||
|
palette refresh all shipped in v0.20.0 + v0.21.0. What stays open:
|
||||||
|
|
||||||
Seven phases landed across `b8fb3fb` → `924a1e2` in v0.11.0; v0.13.0's `7ed4f2c` consumes the per-theme `back.svg`; v0.14.0's `ba527de` adds preview thumbnails. End-to-end:
|
- *Replay-overlay screen-takeover redesign — closed 2026-05-08
|
||||||
|
across 13 commits (v0.21.4–v0.21.7).* The full mockup
|
||||||
|
(`docs/ui-mockups/replay-overlay-mobile.html`) has shipped:
|
||||||
|
banner chrome (v0.21.0), floating MOVE chip (v0.21.2), WIN
|
||||||
|
MOVE scrub-bar marker (post-v0.21.3), playback controls /
|
||||||
|
Space accelerator (post-v0.21.3), scrub notches + labels +
|
||||||
|
keybind footer + ESC / ← / → accelerators + HC border
|
||||||
|
(v0.21.5), Move Log panel + HC scrub track + continuous
|
||||||
|
scrub (v0.21.6), and full-screen 50 % opacity dim layer
|
||||||
|
(v0.21.7). Every major B-2 sub-piece is now closed. The
|
||||||
|
only remaining items are minor polish: notch-label centering
|
||||||
|
and WIN MOVE HC contrast bump (see Open next-step menu).*
|
||||||
|
- *Floating `MOVE N/M` chip above the focused card during
|
||||||
|
playback — closed 2026-05-08 by `2fb2d63`.* World-space
|
||||||
|
`Text2d` entity sibling to the banner overlay; uses the same
|
||||||
|
`LayoutResource` pile coordinates so it survives window
|
||||||
|
resizes without UI/camera math.
|
||||||
|
- *Toast Warning variant wiring — closed 2026-05-08 by `279e23d`.*
|
||||||
|
Daily-challenge-expiry toast fires once per `daily.date` when
|
||||||
|
within 30 min of UTC midnight reset and today is incomplete.
|
||||||
|
`ToastVariant` is now fully load-bearing (every variant has at
|
||||||
|
least one real driver). Future Warning drivers can either reuse
|
||||||
|
the generic `WarningToastEvent(String)` carrier or add their
|
||||||
|
own domain message + `animation_plugin` handler.
|
||||||
|
- *Toast Error variant wiring — closed 2026-05-08 by `68d50b5`.*
|
||||||
|
`MoveRejectedEvent` now fires a 2-second pink-bordered
|
||||||
|
"Invalid move" toast as the third leg of the
|
||||||
|
audio + visual + text rejection-feedback stool.
|
||||||
|
- *High-contrast accessibility mode — closed 2026-05-08 by
|
||||||
|
`c5787c6` + `07e0357` (engine + UI) + v0.21.2's HC chrome
|
||||||
|
rollout (`c9af1ea` + `d87761d` + `ec804d5`) + post-cut
|
||||||
|
dynamic-paint rollout (`c153363`).* Card text rendering plus
|
||||||
|
8 static-border chrome surfaces (modal scaffold, tooltip,
|
||||||
|
onboarding key chips, help panel key chips, stats panel
|
||||||
|
cells, home Level/XP/Score row, home mode buttons, home
|
||||||
|
mode-hotkey chips, 4 settings panel surfaces) all boost
|
||||||
|
borders to `BORDER_SUBTLE_HC` under HC via the
|
||||||
|
`HighContrastBorder` marker. The previously-carved-out
|
||||||
|
dynamic-paint sites are now also covered: HUD action buttons
|
||||||
|
and modal buttons take the same marker (their paint cycles
|
||||||
|
only mutate `BackgroundColor`, so no race); the radial menu
|
||||||
|
rim folds HC into its per-frame spawn via
|
||||||
|
`radial_rim_outline` so the focused rim boosts to
|
||||||
|
`BORDER_SUBTLE_HC` under HC (preserving focused-vs-resting
|
||||||
|
hierarchy that naive marker substitution would invert).
|
||||||
|
- *Reduced-motion mode — closed 2026-05-08 by `c5787c6` +
|
||||||
|
v0.21.2's `ed152e2`.* `effective_slide_secs` forces 0 on
|
||||||
|
card animations; `pulse_splash_cursor` skips the per-frame
|
||||||
|
pulse multiplier; `spawn_splash` skips the scanline overlay
|
||||||
|
entirely. Future scope: gate any future card-lift z-bump
|
||||||
|
animation, warning-chip pulse (when one materialises).
|
||||||
|
|
||||||
- **Bundled default theme** ships in the binary via `embedded://` — 52 hayeah/playing-cards-assets SVGs + a midnight-purple `back.svg`.
|
### Carried forward from v0.19.0
|
||||||
- **User themes** under `themes://`. Drop a directory containing `theme.ron` + 53 SVGs.
|
|
||||||
- **Importer** at `solitaire_engine::theme::import_theme(zip)` validates archives and atomically unpacks.
|
- *App icon round — closed 2026-05-08 by `3eb3a26` + `716a025`.*
|
||||||
- **Picker UI** in Settings → Cosmetic; thumbnails + the active theme's `back` override the legacy `back_N.png` picker when present.
|
Runtime `Window::icon` wired (Linux/macOS/Windows); 9-size
|
||||||
|
PNG hierarchy at `assets/icon/icon_<size>.png` covers Linux
|
||||||
|
hicolor + downstream `.icns`/`.ico` packaging needs. The
|
||||||
|
`.ico` and `.icns` bundle-format files themselves are *not*
|
||||||
|
generated — both would need new crate deps (`ico` and
|
||||||
|
`icns` respectively) and only matter at app-bundle time
|
||||||
|
(cargo-bundle / packaging), not at `cargo run`. Open if the
|
||||||
|
project later ships as a packaged macOS / Windows app.
|
||||||
|
|
||||||
|
### Other small candidates
|
||||||
|
|
||||||
|
- *Play-by-Seed dialog — closed 2026-05-08 by `0cb1587`.*
|
||||||
|
`PlayBySeedPlugin` adds a numeric-input modal with async solver
|
||||||
|
preview (debounced 500 ms). `HomeMode::PlayBySeed` card fires
|
||||||
|
`StartPlayBySeedRequestEvent`. 5 unit tests. 75 new verified-win
|
||||||
|
seeds (`2062bd0`) expand `CHALLENGE_SEEDS` via the new
|
||||||
|
`solitaire_assetgen::gen_seeds` binary.
|
||||||
|
- *Prev/Next selector chips spawn site — closed 2026-05-08 by
|
||||||
|
`a449f60`.* `ReplayPrevButton` / `ReplayNextButton` /
|
||||||
|
`ReplaySelectorCaption` / `ReplaySelectorDetail` now spawn in
|
||||||
|
`spawn_stats_screen` as a compact chip row above the Watch
|
||||||
|
Replay action. The Shareable badge is in the detail line.
|
||||||
|
The click handler and repaint systems were already live since
|
||||||
|
v0.19.0; this was purely the missing spawn site.
|
||||||
|
- **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. As of v0.21.0 origin matches local; the next
|
||||||
|
push happens when post-cut work accumulates and is ready to roll
|
||||||
|
into a v0.21.1 / v0.22.0 cut.
|
||||||
|
|
||||||
|
### 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`), brick-red primary CTA (`#a54242` —
|
||||||
|
swapped from cyan `#6fc2ef` in v0.21.0 commit `a292a7e`), 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 swaps red → lime `#acc267`
|
||||||
|
(was red → cyan pre-v0.21.0; lime is the next-best non-red
|
||||||
|
base16-eighties accent now that the primary itself is red).
|
||||||
|
- **Card glyphs render upright in both corners** — no 180°
|
||||||
|
inverted-corner-indicator rotation. Single-orientation
|
||||||
|
digital play doesn't benefit from the traditional flip-
|
||||||
|
readback convention. `design-system.md` § Game Cards
|
||||||
|
documents this deliberate deviation.
|
||||||
|
|
||||||
## Resume prompt
|
## Resume prompt
|
||||||
|
|
||||||
```
|
```
|
||||||
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||||
Working directory: <Rusty_Solitaire clone path on this machine — local
|
Working directory: <Rusty_Solitaire clone path on this machine>.
|
||||||
directory may still be named Rusty_Solitare from earlier; that's fine>.
|
Branch: master. v0.21.8 is tagged at c50eaf8 (cut 2026-05-08,
|
||||||
Branch: master. Direction is OPEN — v0.14.0 just shipped covering the
|
replay-overlay polish). Seven post-cut commits are on master (see
|
||||||
Quat bug fixes, the v0.13.0 candidate tail, and the entire
|
"Since the v0.21.8 cut" above); push of the last four pending.
|
||||||
replay-pipeline feature.
|
v0.21.7 stays at da3e542, v0.21.6 at f63db76, v0.21.5 at a2432df,
|
||||||
|
v0.21.4 at 23ff62c, v0.21.3 at 3d92a91, v0.21.2 at f23df3b,
|
||||||
|
v0.21.1 at daa655a, v0.21.0 at 04f9bf9.
|
||||||
|
Working tree: uncommitted CHANGELOG + SESSION_HANDOFF docs; push
|
||||||
|
pending. See CHANGELOG.md § [0.21.9] for full detail.
|
||||||
|
|
||||||
State: HEAD at v0.14.0. Working tree clean apart from untracked
|
State: HEAD locally — see `git rev-parse HEAD`. Workspace
|
||||||
CARD_PLAN.md (intentional).
|
tests: 1292 passing / 0 failing. Clippy clean.
|
||||||
Build: cargo clippy --workspace --all-targets -- -D warnings clean.
|
|
||||||
Tests: 1134 passed / 0 failed.
|
|
||||||
|
|
||||||
READ FIRST (in order, before doing anything):
|
READ FIRST (in order, before doing anything):
|
||||||
1. SESSION_HANDOFF.md — v0.14.0 changelog + open punch list
|
1. SESSION_HANDOFF.md — this file
|
||||||
2. CHANGELOG.md — release-by-release record
|
2. CHANGELOG.md — [0.21.9] section has the pending-cut items
|
||||||
3. CLAUDE.md — hard rules (UI-first, no panics, etc.)
|
3. CLAUDE.md — unified-3.0 rule set
|
||||||
4. ARCHITECTURE.md — crate responsibilities + data flow
|
4. CLAUDE_SPEC.md — formal architecture spec
|
||||||
5. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
5. ARCHITECTURE.md — crate responsibilities + data flow
|
||||||
— saved feedback / project context (machine-local;
|
6. docs/ui-mockups/ — design system + 24-mockup library +
|
||||||
may be missing on a fresh machine)
|
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:
|
DECISION TO ASK THE PLAYER FIRST:
|
||||||
A. Smoke-test v0.14.0 on the alex machine first to confirm the
|
A. Android follow-ups — JNI ClipboardManager bridge (arboard
|
||||||
three Quat bug fixes hold up in real gameplay and the replay
|
has no Android backend), Android Keystore (blocked on Phase 8).
|
||||||
pipeline works end-to-end (record → upload → web viewer).
|
Launch verification + double-tap are closed.
|
||||||
B. Take the deferred Bevy-audio-feature trim (Quat investigation
|
B. Phase 8 (sync) — local storage scaffolding, self-hosted
|
||||||
#2) — one-line workspace edit, ~50 fewer transitive crates.
|
Axum server, `SolitaireServerClient` impl. The biggest open
|
||||||
C. Take the deferred solver toggle (Quat investigation #1): add
|
arc by scope; rolls up Android dependencies (Keystore,
|
||||||
"Winnable deals only" Settings toggle. Larger.
|
ClipboardManager).
|
||||||
D. Promote the in-engine "Watch replay" button to real playback.
|
C. Play-by-Seed polish — the dialog is functional but has no
|
||||||
E. Pick from the remaining "next-round candidates" in this doc.
|
visual preview of the solver verdict in the UI yet; the
|
||||||
F. Take the deferred desktop-packaging item (needs artwork +
|
HomeMode card is wired but the dialog spawn site and verdict
|
||||||
signing certs from the user).
|
display could use a second pass.
|
||||||
|
|
||||||
WORKFLOW NOTES:
|
WORKFLOW NOTES:
|
||||||
- Commits use:
|
- Use the system git config (already correct).
|
||||||
git -c user.name=funman300 -c user.email=root@vscode.infinity \
|
- When attributing playtester feedback in commits/docs, use
|
||||||
commit -m "..."
|
"Quat" not "Rhys" (saved feedback memory).
|
||||||
- When attributing playtester feedback in commits/docs, use "Quat"
|
|
||||||
not "Rhys" (saved feedback memory).
|
|
||||||
- Sub-agents stage + verify only; orchestrator commits.
|
- Sub-agents stage + verify only; orchestrator commits.
|
||||||
- Every commit must pass build / clippy / test before pushing.
|
- Every commit must pass build / clippy / test before pushing.
|
||||||
- Push to GitHub (origin) — that is the canonical remote.
|
- Push to GitHub (origin) — gh auth setup-git wired on
|
||||||
|
primary dev box; verify on laptop before first push.
|
||||||
|
- Token-port pattern: when migrating tokens, walk every
|
||||||
|
concrete artifact downstream of the token (PNG textures,
|
||||||
|
embedded SVGs, hardcoded literals, comment color names),
|
||||||
|
not just the token name. v0.21.0 surfaced three "the
|
||||||
|
migration walked past this" follow-ups that all matched
|
||||||
|
this shape — codified here so future similar work can
|
||||||
|
pattern-match instead of rediscovering.
|
||||||
|
- Doc-vs-implementation drift pattern: v0.21.1's pile-marker
|
||||||
|
visibility fix (`4d48cad`) implemented an invariant that
|
||||||
|
had been declared in a module doc comment but was never
|
||||||
|
enforced in code. When future work touches a module with
|
||||||
|
a "this does X" doc comment, verify the code actually does
|
||||||
|
X and add a test if not. Two layers, two checks.
|
||||||
|
|
||||||
OPEN AT THE START: ask which of A–F. Don't pick unilaterally.
|
OPEN AT THE START: ask which of A–C. Don't pick unilaterally.
|
||||||
|
Note: every remaining option is multi-session by nature (A is
|
||||||
|
gated on Android tooling; B and C are explicitly multi-session
|
||||||
|
arcs). A fresh session is a better fit for any of them than the
|
||||||
|
tail of a long working stretch.
|
||||||
```
|
```
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 365 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 369 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 759 B |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 927 B |
@@ -0,0 +1,228 @@
|
|||||||
|
# Android build — developer setup
|
||||||
|
|
||||||
|
This doc captures the toolchain install + build invocation for the
|
||||||
|
Android target. Steps are runnable on a fresh Debian 13 (trixie) box;
|
||||||
|
later sections document what's known to compile, what's stubbed, and
|
||||||
|
the next milestones.
|
||||||
|
|
||||||
|
> **Status (2026-05-07):** First working APK at `fb8b2ac`. 54 MB
|
||||||
|
> debug-signed `solitaire-quest.apk` for `x86_64-linux-android`. Has
|
||||||
|
> NOT yet been verified to launch on a device or emulator — that's
|
||||||
|
> the next milestone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Toolchain install (Debian 13 / trixie)
|
||||||
|
|
||||||
|
Run as one block. Will pull ~15-20 GB of disk between APT, the SDK,
|
||||||
|
the NDK, the system image, and Rust target sysroots. Requires sudo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. JDK 21 (Android tooling needs JDK 17+; Debian 13 default is 21).
|
||||||
|
sudo apt update && sudo apt install -y openjdk-21-jdk-headless unzip wget
|
||||||
|
|
||||||
|
# 2. SDK directory + Google's cmdline-tools bootstrap.
|
||||||
|
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||||
|
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||||
|
wget -O /tmp/cmdline-tools.zip \
|
||||||
|
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
||||||
|
unzip -q /tmp/cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools"
|
||||||
|
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
||||||
|
rm /tmp/cmdline-tools.zip
|
||||||
|
|
||||||
|
# 3. Persist env vars.
|
||||||
|
{
|
||||||
|
echo ''
|
||||||
|
echo '# Android dev'
|
||||||
|
echo 'export ANDROID_HOME="$HOME/Android/Sdk"'
|
||||||
|
echo 'export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/26.3.11579264"'
|
||||||
|
echo 'export JAVA_HOME="$(dirname $(dirname $(readlink -f $(which java))))"'
|
||||||
|
echo 'export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator"'
|
||||||
|
} >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
|
||||||
|
# 4. Accept SDK licences (interactive prompts answered by `yes |`).
|
||||||
|
yes | sdkmanager --licenses
|
||||||
|
|
||||||
|
# 5. Platform packages — ~5 GB.
|
||||||
|
sdkmanager \
|
||||||
|
"platform-tools" \
|
||||||
|
"platforms;android-34" \
|
||||||
|
"build-tools;34.0.0" \
|
||||||
|
"ndk;26.3.11579264" \
|
||||||
|
"emulator" \
|
||||||
|
"system-images;android-34;google_apis;x86_64"
|
||||||
|
|
||||||
|
# 6. AVD for testing (one-time).
|
||||||
|
echo no | avdmanager create avd \
|
||||||
|
-n bevy_test \
|
||||||
|
-k "system-images;android-34;google_apis;x86_64" \
|
||||||
|
-d pixel_7
|
||||||
|
|
||||||
|
# 7. Rust cross-compile targets.
|
||||||
|
rustup target add \
|
||||||
|
aarch64-linux-android \
|
||||||
|
armv7-linux-androideabi \
|
||||||
|
x86_64-linux-android \
|
||||||
|
i686-linux-android
|
||||||
|
|
||||||
|
# 8. cargo-apk.
|
||||||
|
cargo install cargo-apk
|
||||||
|
```
|
||||||
|
|
||||||
|
Sanity:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java --version | head -1 # openjdk 21.0.x
|
||||||
|
adb --version | head -1 # 35.x or higher
|
||||||
|
sdkmanager --list_installed | head # build-tools, emulator, ndk, platforms, system-images
|
||||||
|
avdmanager list avd | head # bevy_test
|
||||||
|
rustup target list --installed | grep android # 4 targets
|
||||||
|
cargo apk --help | head -5
|
||||||
|
```
|
||||||
|
|
||||||
|
If `sdkmanager --version` errors with `JAVA_HOME is not set`, the env
|
||||||
|
section in step 3 didn't apply to your shell — `source ~/.bashrc`
|
||||||
|
again or open a new terminal.
|
||||||
|
|
||||||
|
### Optional: emulator runtime libs
|
||||||
|
|
||||||
|
The Android emulator is dynamically linked against X11/GL/audio. If
|
||||||
|
`emulator -list-avds` works but `emulator -avd bevy_test` complains
|
||||||
|
about `libX11.so.6`, install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y \
|
||||||
|
libx11-6 libxcursor1 libxrandr2 libxi6 libxinerama1 libxxf86vm1 \
|
||||||
|
libgl1 libnss3 libpulse0 libxcomposite1
|
||||||
|
```
|
||||||
|
|
||||||
|
Headless emulator launch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
|
||||||
|
adb wait-for-device && adb devices
|
||||||
|
# Stop later:
|
||||||
|
# adb -s emulator-5554 emu kill
|
||||||
|
```
|
||||||
|
|
||||||
|
Headless + software rendering is fine for "does it boot" smoke tests
|
||||||
|
but useless for perf measurement — use a physical Pixel-class device
|
||||||
|
over USB for real numbers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Build the APK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo apk build -p solitaire_app --target x86_64-linux-android
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
target/debug/apk/solitaire-quest.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
Targets shipped via `[package.metadata.android].build_targets` in
|
||||||
|
`solitaire_app/Cargo.toml`:
|
||||||
|
|
||||||
|
| Target | Use |
|
||||||
|
|--------|-----|
|
||||||
|
| `aarch64-linux-android` | Real phones (modern 64-bit ARM) |
|
||||||
|
| `armv7-linux-androideabi` | Older 32-bit ARM phones |
|
||||||
|
| `x86_64-linux-android` | The `bevy_test` AVD on this dev box |
|
||||||
|
|
||||||
|
Build any of them with `--target <triple>`.
|
||||||
|
|
||||||
|
### Known cosmetic warning
|
||||||
|
|
||||||
|
After the APK is signed cargo-apk panics with:
|
||||||
|
|
||||||
|
```
|
||||||
|
thread 'main' panicked: Bin is not compatible with Cdylib
|
||||||
|
```
|
||||||
|
|
||||||
|
This happens AFTER the APK is on disk and signed. cargo-apk is
|
||||||
|
trying to also wrap the desktop `[[bin]]` target. The APK is still
|
||||||
|
valid. Work around with `--lib`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
|
||||||
|
```
|
||||||
|
|
||||||
|
(Permanent fix to come — likely a `[[bin]] required-features = ["desktop"]`
|
||||||
|
gate so cargo-apk skips the bin target on Android.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Install + run
|
||||||
|
|
||||||
|
Physical device:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb devices # confirm connection
|
||||||
|
adb install target/debug/apk/solitaire-quest.apk
|
||||||
|
adb shell am start -n com.solitairequest.app/android.app.NativeActivity
|
||||||
|
adb logcat | grep -iE "RustStdoutStderr|solitaire|panic"
|
||||||
|
```
|
||||||
|
|
||||||
|
Emulator:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
|
||||||
|
adb wait-for-device
|
||||||
|
adb install target/debug/apk/solitaire-quest.apk
|
||||||
|
# ... same start + logcat steps as above.
|
||||||
|
```
|
||||||
|
|
||||||
|
If `adb install` errors with `INSTALL_FAILED_NO_MATCHING_ABIS`, the
|
||||||
|
emulator is x86_64 but the APK was built for arm — rebuild with the
|
||||||
|
`x86_64-linux-android` target, or add an x86_64 system image to the
|
||||||
|
AVD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. What's wired vs. what's stubbed
|
||||||
|
|
||||||
|
The first build pass (commit `fb8b2ac`) gates four desktop-only
|
||||||
|
crates / call sites so the workspace cross-compiles. Each gate is
|
||||||
|
documented at its call site.
|
||||||
|
|
||||||
|
| Surface | Desktop | Android |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| Bevy windowing | x11 + wayland | `android-native-activity` (NativeActivity glue) |
|
||||||
|
| Clipboard ("Copy share link") | `arboard` writes URL | Toast surfaces the URL inline |
|
||||||
|
| OS keychain (JWT tokens) | `keyring` v4 → Secret Service / Keychain / Credential Store | Stub returning `KeychainUnavailable`; sync requires fresh login each launch |
|
||||||
|
| App entry point | `bin` target → `solitaire_app::run()` | `cdylib` target loaded by NativeActivity |
|
||||||
|
|
||||||
|
What's NOT yet ported / not yet measured:
|
||||||
|
|
||||||
|
- `dirs::data_dir()` returns `None` on Android. Callers in
|
||||||
|
`solitaire_data/src/storage.rs`, `progress.rs`, `replay.rs`,
|
||||||
|
`achievements.rs`, `settings.rs` all need an Android-aware
|
||||||
|
helper (likely `/data/data/com.solitairequest.app/files`).
|
||||||
|
- Touch UX pass — hit-target sizes, modal scaling on small screens,
|
||||||
|
app lifecycle (suspend / resume), font scaling.
|
||||||
|
- Android Keystore via JNI for `auth_tokens`.
|
||||||
|
- JNI ClipboardManager for share links.
|
||||||
|
- Google Play Games sign-in (the `solitaire_gpgs` crate referenced
|
||||||
|
in older docs doesn't yet exist).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Iteration loop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit code…
|
||||||
|
cargo build -p solitaire_app # desktop sanity
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings # gate
|
||||||
|
cargo test --workspace # gate
|
||||||
|
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
|
||||||
|
adb install -r target/debug/apk/solitaire-quest.apk # `-r` reinstalls
|
||||||
|
adb logcat -c && adb shell am start -n com.solitairequest.app/android.app.NativeActivity
|
||||||
|
adb logcat | grep -iE "RustStdoutStderr|solitaire"
|
||||||
|
```
|
||||||
|
|
||||||
|
`adb logcat` is the canonical way to see Bevy / Rust panic output —
|
||||||
|
they end up in the `RustStdoutStderr` tag.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Android Playability TODO
|
||||||
|
|
||||||
|
**Started:** 2026-05-10 — first hardware screenshot of v0.22.3 APK
|
||||||
|
running on a real device showed the desktop HUD projected onto a
|
||||||
|
360 dp portrait viewport with no mobile adaptation. This list
|
||||||
|
tracks the work needed to make the APK genuinely playable, not
|
||||||
|
just "boots without crashing."
|
||||||
|
|
||||||
|
**Context:** v0.22.3 (signed release APK) builds and launches.
|
||||||
|
JNI bridges (clipboard, keystore) compile but are untested on
|
||||||
|
hardware. The work below is UI/UX port work — no architectural
|
||||||
|
rewrites required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reading from the v0.22.3 screenshot
|
||||||
|
|
||||||
|
| Region | Observation |
|
||||||
|
|--------|-------------|
|
||||||
|
| Top ~5 % | System bar (clock, signal, battery) overlapped by game HUD — no safe-area inset |
|
||||||
|
| HUD text row | `Score:0 Pause Esc Help A Modes [] New_Game N Moves:0 0:08` all overlapping — desktop layout crammed into 360 dp |
|
||||||
|
| Keyboard hints | `Esc`, `A`, `[]`, `N` shown next to buttons — meaningless on touch |
|
||||||
|
| Foundations row | Leftmost foundation (♥) clipped left; rightmost tableau column (♠ 4) clipped right |
|
||||||
|
| Card backs | Face-down cards render as solid red squares, not back-art texture |
|
||||||
|
| Vertical use | Cards occupy top ~30 % only; bottom 70 % empty black — no portrait-aware layout |
|
||||||
|
| Bottom edge | No accommodation for Android gesture / home-indicator area |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — Blocking playability
|
||||||
|
|
||||||
|
- [x] **Safe-area insets (top + bottom).** *Closed 2026-05-10 by
|
||||||
|
`b9aa262`.* `SafeAreaInsets` resource + `SafeAreaInsetsPlugin`
|
||||||
|
query `WindowInsets.getInsets(systemBars())` via JNI on Android;
|
||||||
|
HUD anchors carry `SafeAreaAnchoredTop { base_top }` and the
|
||||||
|
change-detection fix-up system re-applies `base_top + insets.top`
|
||||||
|
whenever the resource updates. Bottom inset is captured but not
|
||||||
|
yet consumed (waits for bottom-anchored UI).
|
||||||
|
- [x] **Mobile HUD layout.** *Closed 2026-05-10.* Both the left HUD
|
||||||
|
column and the right action button row are now capped at
|
||||||
|
`max_width: 50 %` and the button row + tier-row child Nodes carry
|
||||||
|
`flex_wrap: Wrap`. On a 360 dp viewport the 6-button row breaks
|
||||||
|
to multiple lines (right-justified) and the tier rows wrap
|
||||||
|
individually instead of overflowing into the action column. On
|
||||||
|
desktop (≥ 1280 px) the 50 % cap is wider than any natural row
|
||||||
|
width so the existing single-line layout is unchanged.
|
||||||
|
- [x] **Card-back asset not rendering.** *Closed 2026-05-10 by
|
||||||
|
`fcc7337`.* `AssetPlugin::file_path = "../assets"` was set
|
||||||
|
unconditionally to fix the desktop `cargo run -p solitaire_app`
|
||||||
|
CWD relativity, but on Android cargo-apk packages the same
|
||||||
|
directory into the APK at `assets/` and Bevy's
|
||||||
|
AndroidAssetReader is already rooted there — prepending `../`
|
||||||
|
walked the reader out of the APK assets root and every load
|
||||||
|
failed silently. The face-down branch then fell through to the
|
||||||
|
`card_back_colour(0)` solid-red brick fallback. Gated the
|
||||||
|
override behind `#[cfg(not(target_os = "android"))]`.
|
||||||
|
- [x] **Viewport overflow.** *Closed 2026-05-10.* `compute_layout`
|
||||||
|
was clamping the input window up to `MIN_WINDOW = 800 × 600`,
|
||||||
|
so a 360 dp phone got laid out as if it were 800-wide and the
|
||||||
|
outer piles fell outside the actual viewport. Lowered the floor
|
||||||
|
to 320 × 400 (below the smallest reasonable phone) so real
|
||||||
|
Android resolutions flow through without clamping, while keeping
|
||||||
|
a sentinel to guard against degenerate / startup-zero windows.
|
||||||
|
New regression test `phone_portrait_layout_fits_horizontally`
|
||||||
|
asserts all 13 piles fit a 360 × 800 viewport.
|
||||||
|
|
||||||
|
## P1 — Touch UX
|
||||||
|
|
||||||
|
- [x] **Suppress keyboard-hint labels on Android.** *Closed
|
||||||
|
2026-05-10.* `spawn_action_button` now nulls the `hotkey`
|
||||||
|
argument on Android via a `#[cfg(target_os = "android")]` rebind,
|
||||||
|
so the U / Esc / F1 / N chips next to the action row labels
|
||||||
|
disappear on touch builds. Other hint sites (onboarding panel,
|
||||||
|
pause-modal `Esc` hint, mode-card hotkey chips on the home
|
||||||
|
screen, replay overlay footer, modal toggle hints in
|
||||||
|
profile/stats/leaderboard/settings, help screen) survive — they
|
||||||
|
live behind navigation and a touch user reaches them less often.
|
||||||
|
Track as a P3 sweep when more screens are audited on hardware.
|
||||||
|
- [x] **Thumb-sized hit targets.** *Closed 2026-05-10.* Action
|
||||||
|
button Node carries `min_width: Val::Px(48.0), min_height:
|
||||||
|
Val::Px(48.0)` — meets Material's 48 dp baseline on touch and is
|
||||||
|
a no-op for buttons whose content already exceeds 48 px in
|
||||||
|
either axis. Applied universally rather than cfg-gated since
|
||||||
|
Material's guideline applies to all input modes. Cards, pile
|
||||||
|
markers, modal close buttons not yet audited — track as P3 if
|
||||||
|
they fall below threshold on hardware.
|
||||||
|
- [ ] **Portrait-first card spacing.** Stretch tableau piles vertically
|
||||||
|
to fill height; reduce inter-pile gaps so 7 columns fit in 360 dp.
|
||||||
|
- [ ] **Double-tap auto-move visible feedback.** `handle_double_tap`
|
||||||
|
exists since `395a322` — verify it triggers on hardware and add a
|
||||||
|
brief source-card flash / highlight to confirm to the user.
|
||||||
|
|
||||||
|
## P2 — Polish
|
||||||
|
|
||||||
|
- [ ] **Drag responsiveness on touch.** Bevy default touch-to-mouse
|
||||||
|
mapping can lag; confirm drag start threshold isn't too high for a
|
||||||
|
finger.
|
||||||
|
- [ ] **Long-press menu.** Alternative to right-click (which doesn't
|
||||||
|
exist on touch). Wire to the existing right-click-highlight system.
|
||||||
|
- [ ] **HUD typography.** Reduce text sizes for `Score:`, `Moves:`,
|
||||||
|
timer so they fit cleanly in one row.
|
||||||
|
- [ ] **Orientation lock.** Set `android:screenOrientation="portrait"`
|
||||||
|
in cargo-apk manifest (or design a landscape layout).
|
||||||
|
|
||||||
|
## P3 — Asset density
|
||||||
|
|
||||||
|
- [ ] **Density-aware card scaling.** Currently single texture size; on
|
||||||
|
a high-DPI phone the cards look small. Scale by
|
||||||
|
`Window::scale_factor()` or ship multiple PNG sizes.
|
||||||
|
- [ ] **App-icon density buckets.** Nine sizes already exist in
|
||||||
|
`assets/icon/`; verify the manifest references them so Android's
|
||||||
|
launcher picks the right one.
|
||||||
|
|
||||||
|
## P4 — Stability / runtime
|
||||||
|
|
||||||
|
- [ ] **B0004 ECS hierarchy warnings.** Flagged in
|
||||||
|
`SESSION_HANDOFF.md` after APK launch verification — investigate
|
||||||
|
whether they cause gameplay bugs on hardware vs. AVD.
|
||||||
|
- [ ] **AVD functional tests for JNI bridges.** Clipboard (`2c822ba`)
|
||||||
|
and Keystore (`f281425`) shipped but never tested on real device
|
||||||
|
or AVD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes / decisions
|
||||||
|
|
||||||
|
* This list is screenshot-driven; expect more items to surface once
|
||||||
|
P0 unblocks actually moving cards on hardware.
|
||||||
|
* The pattern across all the bugs is "no one ran the relevant code
|
||||||
|
path on Android yet." The hard work — Bevy 0.18 on Android,
|
||||||
|
JNI bridges, signed CI builds — is done. What's left is a
|
||||||
|
coordinated pass of `#[cfg(target_os = "android")]` gates plus
|
||||||
|
making `LayoutResource` query the real surface size.
|
||||||
|
* Where possible, prefer responsive layout (query window size) over
|
||||||
|
branching `#[cfg]` blocks. Branches are fine for input methods
|
||||||
|
(touch vs. mouse) but not for screen geometry — a foldable or
|
||||||
|
desktop window of equivalent size should look the same.
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>Rusty Solitaire - Achievements</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500;700&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"background": "#101417",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"surface": "#151515",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"surface-container-low": "#181c1f"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"action-bar-height": "64px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #151515;
|
||||||
|
color: #e0e3e6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.scanline {
|
||||||
|
background: linear-gradient(to bottom, transparent 50%, rgba(0,0,0,0.1) 50%);
|
||||||
|
background-size: 100% 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="font-body-md text-body-md overflow-x-hidden pb-[action-bar-height]">
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<header class="fixed top-0 w-full h-[32px] bg-[#202020] flex items-center justify-between px-margin-edge z-[60] border-b border-outline-variant">
|
||||||
|
<div class="flex items-center gap-2 font-label-caps text-on-surface">
|
||||||
|
<span class="text-primary">▌</span>achievements.json
|
||||||
|
</div>
|
||||||
|
<div class="font-label-caps text-[#a0a0a0]">
|
||||||
|
8/19 UNLOCKED
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Top App Bar (Shared Component Reference) -->
|
||||||
|
<nav class="fixed top-[32px] w-full h-[64px] bg-surface flex items-center justify-between px-margin-edge z-50 border-b border-outline-variant">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-primary" data-icon="terminal">terminal</span>
|
||||||
|
<h1 class="font-headline text-[20px] text-primary uppercase tracking-widest">Rusty Solitaire</h1>
|
||||||
|
</div>
|
||||||
|
<button class="w-10 h-10 flex items-center justify-center hover:bg-surface-container-highest transition-colors">
|
||||||
|
<span class="material-symbols-outlined text-on-surface-variant" data-icon="settings">settings</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<main class="mt-[112px] px-margin-edge">
|
||||||
|
<!-- Hero Progress Card -->
|
||||||
|
<section class="w-full h-[100px] bg-[#202020] border border-[#353535] rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<span class="font-label-caps text-[10px] text-[#a0a0a0]">PROGRESS</span>
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<span class="font-headline text-[28px] font-bold text-[#d0d0d0]">8/19</span>
|
||||||
|
<span class="font-label-caps text-[14px] text-highlight-celebration">(42%)</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-[4px] bg-[#353535] rounded-full overflow-hidden mt-1">
|
||||||
|
<div class="h-full bg-highlight-celebration" style="width: 42%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Filter Chip Row -->
|
||||||
|
<section class="flex gap-2 mb-6 overflow-x-auto no-scrollbar">
|
||||||
|
<button class="h-[32px] px-3 flex items-center justify-center border border-[#6fc2ef] text-[#6fc2ef] rounded-[4px] font-label-caps text-[11px]">
|
||||||
|
[ ALL ]
|
||||||
|
</button>
|
||||||
|
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
|
||||||
|
UNLOCKED
|
||||||
|
</button>
|
||||||
|
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
|
||||||
|
LOCKED
|
||||||
|
</button>
|
||||||
|
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
|
||||||
|
SECRET
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
<!-- Achievements Grid -->
|
||||||
|
<section class="grid grid-cols-2 gap-3 mb-10">
|
||||||
|
<!-- FIRST WIN -->
|
||||||
|
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="emoji_events" style="font-variation-settings: 'FILL' 1;">emoji_events</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">FIRST WIN</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Win your first game</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- SPEED DEMON -->
|
||||||
|
<div class="h-[100px] bg-[#202020] border border-highlight-celebration p-3 flex flex-col justify-between rounded-sm relative">
|
||||||
|
<div class="absolute inset-0 border border-highlight-celebration opacity-20 pointer-events-none"></div>
|
||||||
|
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="speed" style="font-variation-settings: 'FILL' 1;">speed</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">SPEED DEMON</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Win in under 3:00</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- STREAK 10 -->
|
||||||
|
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="bolt" style="font-variation-settings: 'FILL' 1;">bolt</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">STREAK 10</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">10 wins in a row</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- DAILY DEFENDER -->
|
||||||
|
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="calendar_today" style="font-variation-settings: 'FILL' 1;">calendar_today</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">DAILY DEFENDER</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Complete 7 daily seeds</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- PERFECTIONIST (LOCKED) -->
|
||||||
|
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||||
|
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="undo">undo</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">PERFECTIONIST</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Win without using undo</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- CHALLENGE BEATEN (LOCKED) -->
|
||||||
|
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||||
|
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="military_tech">military_tech</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">CHALLENGE BEATEN</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Complete CHALLENGE mode</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- SECRET (LOCKED) -->
|
||||||
|
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||||
|
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="help_outline">help_outline</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">????</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">SECRET · Hidden until unlocked</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- PAR HUNTER (LOCKED) -->
|
||||||
|
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||||
|
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="golf_course">golf_course</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">PAR HUNTER</h3>
|
||||||
|
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Beat par on 50 games</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- Footer Status -->
|
||||||
|
<footer class="fixed bottom-[action-bar-height] w-full h-[24px] bg-background border-t border-outline-variant flex items-center justify-between px-margin-edge z-40 text-[10px] font-label-caps">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-primary mr-1">▌</span>
|
||||||
|
<span class="text-on-surface-variant">NORMAL</span>
|
||||||
|
<span class="mx-2 text-outline">│</span>
|
||||||
|
<span class="text-on-surface-variant">achievements</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div><span class="text-[#a0a0a0]">[F]</span> <span class="text-[#505050]">filter</span></div>
|
||||||
|
<div><span class="text-[#a0a0a0]">[/]</span> <span class="text-[#505050]">search</span></div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Bottom Navigation Bar (Shared Component Reference) -->
|
||||||
|
<nav class="fixed bottom-0 w-full h-action-bar-height bg-surface-container flex justify-around items-center px-margin-edge z-50 border-t border-outline-variant">
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||||
|
<span class="material-symbols-outlined" data-icon="power_settings_new">power_settings_new</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[Q] QUIT</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||||
|
<span class="material-symbols-outlined" data-icon="add_box">add_box</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[N] NEW</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||||
|
<span class="material-symbols-outlined" data-icon="undo">undo</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[U] UNDO</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||||
|
<span class="material-symbols-outlined" data-icon="lightbulb">lightbulb</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[H] HINT</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<!-- CRT Overlay Effect (Visual Decoration) -->
|
||||||
|
<div class="fixed inset-0 pointer-events-none z-[100] opacity-[0.03] scanline"></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,251 @@
|
|||||||
|
# Card-face artwork migration plan
|
||||||
|
|
||||||
|
**Status:** planning artifact (no code changed by this document).
|
||||||
|
**Tracks:** the "Card-face / suit / card-back artwork regeneration"
|
||||||
|
item in `SESSION_HANDOFF.md` → "Visual-identity follow-ups"
|
||||||
|
(SESSION_HANDOFF Resume prompt option D).
|
||||||
|
**Companion to:** `docs/ui-mockups/design-system.md` (Game Cards
|
||||||
|
spec, lines 214–233) and `docs/ui-mockups/desktop-adaptation.md`
|
||||||
|
(rules-based companion to the mockups).
|
||||||
|
|
||||||
|
## Why this is a multi-session arc
|
||||||
|
|
||||||
|
Every post-v0.20.0 visual-identity port to date (modal scaffold,
|
||||||
|
toasts, table chrome, splash boot screen, replay overlay) was a
|
||||||
|
**single rendering path** — change tokens, change comments, ship.
|
||||||
|
Cards have **two** rendering paths that are visually identical
|
||||||
|
today and would visually disagree the moment one moves:
|
||||||
|
|
||||||
|
1. **PNG path (production).** `assets/cards/faces/<rank><suit>.png`
|
||||||
|
loaded into `CardImageSet.faces[suit][rank]` at startup; card
|
||||||
|
sprites blit the texture. 52 face PNGs + 5 back PNGs already
|
||||||
|
in `assets/`, all the legacy white-card aesthetic from the
|
||||||
|
pre-Terminal design system.
|
||||||
|
2. **Constant fallback (tests + asset-missing edge).** When
|
||||||
|
`CardImageSet` isn't a registered resource (the case under
|
||||||
|
`MinimalPlugins` test fixtures, and the bare-bones path the
|
||||||
|
first-frame of production hits before assets resolve), the
|
||||||
|
renderer falls back to solid-colour sprites driven by the
|
||||||
|
`card_plugin` constants:
|
||||||
|
- `CARD_FACE_COLOUR` — `(0.98, 0.98, 0.95)` cream-ish white.
|
||||||
|
- `RED_SUIT_COLOUR` — `(0.78, 0.12, 0.15)` warm red.
|
||||||
|
- `BLACK_SUIT_COLOUR` — `(0.08, 0.08, 0.08)` near-black.
|
||||||
|
- `CARD_FACE_COLOUR_RED_CBM` — `(0.85, 0.92, 1.0, 1.0)` light
|
||||||
|
blue (the legacy color-blind tint).
|
||||||
|
- `card_back_colour(idx)` — five legacy back themes.
|
||||||
|
|
||||||
|
A single-path migration leaves a known-broken state where tests
|
||||||
|
pass against Terminal constants while a human sees legacy artwork
|
||||||
|
on screen — the exact bisection-hostile drift the handoff's
|
||||||
|
"in lockstep" warning preempts.
|
||||||
|
|
||||||
|
## Target state — Terminal aesthetic
|
||||||
|
|
||||||
|
Per `design-system.md` § Game Cards (lines 214–233):
|
||||||
|
|
||||||
|
### Card face
|
||||||
|
|
||||||
|
| Element | Spec |
|
||||||
|
|---|---|
|
||||||
|
| Background | `#1a1a1a` |
|
||||||
|
| Border | 1 px solid in **suit colour** (pink for ♥/♦, foreground gray for ♠/♣) |
|
||||||
|
| Corner radius | 8 px |
|
||||||
|
| Top-left | rank in JetBrains Mono **Bold 18 px** + small suit glyph (10 px) |
|
||||||
|
| Bottom-right | large suit glyph (32 px), rotated 180° |
|
||||||
|
| Glyph fill rule | ♥ ♠ filled; ♦ ♣ outlined (1.5 px stroke). Always on, not a toggle. |
|
||||||
|
|
||||||
|
### Suit colours (always-on glyph differentiation is the *primary*
|
||||||
|
distinguishing mechanism; colour is supplementary):
|
||||||
|
|
||||||
|
| Suit | Default | Color-blind mode |
|
||||||
|
|---|---|---|
|
||||||
|
| Hearts | `#fb9fb1` (pink) | `#6fc2ef` (cyan) |
|
||||||
|
| Diamonds | `#fb9fb1` (pink) | `#6fc2ef` (cyan) |
|
||||||
|
| Spades | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) |
|
||||||
|
| Clubs | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) |
|
||||||
|
|
||||||
|
### Card back ("Terminal" theme)
|
||||||
|
|
||||||
|
| Element | Spec |
|
||||||
|
|---|---|
|
||||||
|
| Background | `#151515` |
|
||||||
|
| Pattern | horizontal scanlines at 2 px pitch in `#1a1a1a` (1 px line, 1 px gap), full bleed |
|
||||||
|
| Border | 1 px solid `#353535` |
|
||||||
|
| Top-left badge | 12×16 px solid `#6fc2ef` block, 6 px from corner |
|
||||||
|
| Bottom-right monogram | `▌RS` in JetBrains Mono 12 px `#505050`, 6 px from corner |
|
||||||
|
| Corner radius | 8 px |
|
||||||
|
| Theme name / author | `"Terminal"` / `"Rusty Solitaire"` |
|
||||||
|
|
||||||
|
## Generation pipeline — programmatic SVG via the existing
|
||||||
|
`resvg` stack
|
||||||
|
|
||||||
|
### Why this path (vs. external tooling or direct `tiny_skia`)
|
||||||
|
|
||||||
|
The codebase already ships an SVG-to-PNG rasteriser at
|
||||||
|
`solitaire_engine/src/assets/svg_loader.rs`:
|
||||||
|
|
||||||
|
- Public `rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result<Image, _>`
|
||||||
|
- Backed by `usvg` (parser) + `resvg` (renderer) + `tiny_skia`
|
||||||
|
(CPU pixmap)
|
||||||
|
- Bundled font db includes JetBrains-style mono (FiraMono — same
|
||||||
|
face the splash uses; close enough to JetBrains Mono for
|
||||||
|
rasterisation purposes, and identical to what the Bevy UI
|
||||||
|
consumes in the rest of the app)
|
||||||
|
- `RenderAssetUsages::default()` is the call-site convention here
|
||||||
|
|
||||||
|
This means: **generating new card PNGs is one new file
|
||||||
|
(`solitaire_engine/examples/card_face_generator.rs`) calling an
|
||||||
|
existing public function.** No new dependencies, no asset-pipeline
|
||||||
|
changes, no build-script machinery. Anyone who runs the example
|
||||||
|
gets bit-identical artwork.
|
||||||
|
|
||||||
|
The two alternatives are weaker:
|
||||||
|
|
||||||
|
- **External tool (Inkscape / Figma / hand-design)** — produces
|
||||||
|
one-off PNGs that can't be re-generated reproducibly without
|
||||||
|
re-opening the source files in a specific tool. Iteration cost
|
||||||
|
is high; design tweaks (e.g. "make the suit glyph 2 px larger")
|
||||||
|
require a designer-in-the-loop.
|
||||||
|
- **Direct `tiny_skia` painting calls** — bypasses SVG entirely,
|
||||||
|
but loses the readability of "open the SVG to see exactly what
|
||||||
|
the card looks like." Also reinvents primitives (rounded
|
||||||
|
rectangles, text layout) that `usvg` already handles.
|
||||||
|
|
||||||
|
### Output format
|
||||||
|
|
||||||
|
PNG, RGBA8 sRGB, **dimensions 256 × 384** (2:3 aspect, half the
|
||||||
|
default `SvgLoaderSettings` of 512 × 768).
|
||||||
|
|
||||||
|
Rationale: cards never exceed ~250 px wide on desktop windows
|
||||||
|
today, and 256 × 384 PNGs are ~6 KB each at this content density
|
||||||
|
(13.4 KB total for a full deck of 52 + 5 backs). The default 512 ×
|
||||||
|
768 is 2× what's needed and quadruples the on-disk asset weight.
|
||||||
|
The existing legacy PNGs are 512 × 768 — reducing the new ones
|
||||||
|
halves the runtime asset size.
|
||||||
|
|
||||||
|
## Lockstep migration — recommended order
|
||||||
|
|
||||||
|
Each step is a separate commit; the constraint is that **steps 4
|
||||||
|
and 5 must land in the same commit** (or at most adjacent commits
|
||||||
|
on the same branch) so the rendered output never diverges between
|
||||||
|
the two paths.
|
||||||
|
|
||||||
|
1. **(Done — this commit)** Land the migration plan doc.
|
||||||
|
2. **Land the SVG generator example.** New
|
||||||
|
`solitaire_engine/examples/card_face_generator.rs`. Output
|
||||||
|
goes to `assets/cards/faces/` and `assets/cards/backs/`. Run
|
||||||
|
once locally to seed the new artwork. The example file stays
|
||||||
|
in-tree as a regenerator for future tweaks.
|
||||||
|
3. **(Optional — can land separately)** Add a one-shot regression
|
||||||
|
test that re-runs the generator into a `tempdir` and compares
|
||||||
|
the resulting bytes against the on-disk artwork; pinning the
|
||||||
|
generator output prevents silent drift if `usvg`/`resvg` ever
|
||||||
|
tweak rendering. Skip if the test runtime cost is unacceptable.
|
||||||
|
4. **Land the new artwork** (PNG bytes from step 2 committed to
|
||||||
|
`assets/cards/`) **and** the constant migration in the *same
|
||||||
|
commit*:
|
||||||
|
- `CARD_FACE_COLOUR` → `Color::srgb(0.102, 0.102, 0.102)` (`#1a1a1a`)
|
||||||
|
- `RED_SUIT_COLOUR` → `Color::srgb(0.984, 0.624, 0.694)` (`#fb9fb1`)
|
||||||
|
- `BLACK_SUIT_COLOUR` → `Color::srgb(0.816, 0.816, 0.816)` (`#d0d0d0`)
|
||||||
|
- `CARD_FACE_COLOUR_RED_CBM` → `Color::srgb(0.435, 0.761, 0.937)` (`#6fc2ef`) — note this is now the colour-blind *suit* colour, not a face tint; semantics shift slightly.
|
||||||
|
- `card_back_colour(idx)` — re-author for the Terminal palette;
|
||||||
|
index 0 stays the canonical "Terminal" back from `design-system.md`.
|
||||||
|
5. **Test updates land in step 4's commit.** The pinning tests at
|
||||||
|
`card_plugin.rs` lines 1749, 1750, 1767, 1768, 2057, 2063,
|
||||||
|
2071, 2081 all assert against the old constants. New
|
||||||
|
assertions update in lockstep with the constant changes.
|
||||||
|
|
||||||
|
## CBM (color-blind mode) semantics shift — flag
|
||||||
|
|
||||||
|
The **legacy** `CARD_FACE_COLOUR_RED_CBM` was a *face tint* — red
|
||||||
|
suits got a light-blue background wash. The **Terminal** spec
|
||||||
|
moves CBM into the *suit colour* itself (red glyphs swap to cyan).
|
||||||
|
Step 4 will rename / repurpose this constant; it's not a 1:1
|
||||||
|
replacement.
|
||||||
|
|
||||||
|
Two options:
|
||||||
|
|
||||||
|
- **Rename + repurpose:** `CARD_FACE_COLOUR_RED_CBM` →
|
||||||
|
`RED_SUIT_COLOUR_CBM`. Communicates the semantic shift in the
|
||||||
|
symbol name. Requires touching every callsite.
|
||||||
|
- **Keep the name, change the meaning:** less code churn but
|
||||||
|
worse for greppability — a future reader hitting the legacy
|
||||||
|
name will assume face-tint behaviour.
|
||||||
|
|
||||||
|
Recommendation: **rename**. The CBM swap is a one-frame operation
|
||||||
|
even if it touches every existing callsite (currently lines 642,
|
||||||
|
2071, 2081 per `grep -n CARD_FACE_COLOUR_RED_CBM`).
|
||||||
|
|
||||||
|
## Theme system — out of scope here
|
||||||
|
|
||||||
|
The card-theme system (`docs/CARD_PLAN.md`, `theme/plugin.rs`)
|
||||||
|
already supports user-supplied themes via `assets/themes/<theme>/`
|
||||||
|
SVG files rasterised by `svg_loader.rs`. The new Terminal artwork
|
||||||
|
is the **default theme**, not a new entry in the theme picker —
|
||||||
|
the theme system continues to overlay user themes on top of the
|
||||||
|
default at runtime.
|
||||||
|
|
||||||
|
If the next session wants to also ship Terminal as a *named theme
|
||||||
|
slot* (so a user can switch back to the legacy artwork via the
|
||||||
|
theme picker), that's an additive change after step 4 and lives
|
||||||
|
in `theme::plugin::apply_theme_to_card_image_set`.
|
||||||
|
|
||||||
|
## Test impact summary
|
||||||
|
|
||||||
|
`grep -n CARD_FACE_COLOUR\\b\|RED_SUIT_COLOUR\\b\|BLACK_SUIT_COLOUR\\b` in
|
||||||
|
`card_plugin.rs`:
|
||||||
|
|
||||||
|
- Line 1749–1750: red-suit text colour assertions (♥ + ♦).
|
||||||
|
- Line 1767–1768: black-suit text colour assertions (♠ + ♣).
|
||||||
|
- Line 2057, 2063: face-colour assertion in default mode.
|
||||||
|
- Line 2071, 2081: face-colour assertion in CBM.
|
||||||
|
|
||||||
|
The four suit-colour and two face-colour tests are **invariant
|
||||||
|
guards** — they exist precisely so a constant tweak surfaces here
|
||||||
|
rather than in a visual review. Step 4 updates each in lockstep
|
||||||
|
with the constant value change. No new test infrastructure
|
||||||
|
needed.
|
||||||
|
|
||||||
|
## Open questions to resolve before step 4
|
||||||
|
|
||||||
|
1. **Border colour conflict.** The spec (line 218) says "Border:
|
||||||
|
1 px solid in suit colour." The fallback path doesn't draw a
|
||||||
|
border today — it draws solid-colour sprites. Step 4 either:
|
||||||
|
(a) leaves the fallback as solid-colour squares (the test
|
||||||
|
environment doesn't visually validate borders anyway), or
|
||||||
|
(b) extends the fallback renderer to paint a 1 px outline.
|
||||||
|
Recommend (a) — fallback fidelity isn't load-bearing.
|
||||||
|
2. **Glyph rendering in the constant fallback.** The fallback
|
||||||
|
today doesn't render suit glyphs at all — it's a coloured
|
||||||
|
square. The spec's filled-vs-outlined glyph differentiation
|
||||||
|
only matters in the PNG path. No change to the constant
|
||||||
|
fallback for glyphs.
|
||||||
|
3. **High-contrast mode.** `design-system.md` line 274 mentions
|
||||||
|
a high-contrast accessibility mode (boosts foreground from
|
||||||
|
`#d0d0d0` to `#f5f5f5`, suit-red from `#fb9fb1` to `#ff8aa0`).
|
||||||
|
Not currently implemented anywhere; out of scope for this
|
||||||
|
migration but worth flagging for a future accessibility pass.
|
||||||
|
|
||||||
|
## Post-migration — what's still open
|
||||||
|
|
||||||
|
- **High-contrast mode** (above).
|
||||||
|
- **Reduced-motion mode** for card lift / drop transitions
|
||||||
|
(also a `design-system.md` accessibility item, separate from
|
||||||
|
artwork).
|
||||||
|
- **The 9 missing-plugin screens** (splash, challenge,
|
||||||
|
time-attack, weekly-goals, leaderboard, sync, level-up,
|
||||||
|
replay, radial-menu) per `project_ui_overhaul` memory still
|
||||||
|
need their plugin ports — separate from the cards arc.
|
||||||
|
|
||||||
|
## Sign-off criteria for "D closed"
|
||||||
|
|
||||||
|
D from the SESSION_HANDOFF Resume prompt is closed when **all of
|
||||||
|
the following hold simultaneously**:
|
||||||
|
|
||||||
|
- The 52 face PNGs + 5 back PNGs in `assets/cards/` are the
|
||||||
|
Terminal-aesthetic artwork (regeneratable via the example).
|
||||||
|
- The five `card_plugin` constants reflect the Terminal palette.
|
||||||
|
- All pinning tests pass against the new values.
|
||||||
|
- A human boots the game and sees Terminal cards (not white
|
||||||
|
cards). This sign-off needs a real `cargo run`, not just
|
||||||
|
`cargo test`.
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=390, height=844, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>Challenge Mode Menu</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;700&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"background": "#101417",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"outline": "#505050",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"surface": "#151515",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-primary": "#003549"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"mono": ["JetBrains Mono", "monospace"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #101417;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.retro-scanline {
|
||||||
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex items-center justify-center min-h-screen text-on-background overflow-hidden">
|
||||||
|
<!-- Mobile Container (390x844) -->
|
||||||
|
<div class="relative w-[390px] h-[844px] bg-background flex flex-col overflow-hidden border border-outline-variant">
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<div class="h-[32px] bg-surface-container flex items-center justify-between px-4 text-[11px] font-mono border-b border-outline-variant shrink-0">
|
||||||
|
<span class="text-suit-black">▌challenge.tsx</span>
|
||||||
|
<span class="text-[#a0a0a0]">LV 12 · UNLOCKED</span>
|
||||||
|
</div>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="h-[80px] px-margin-edge flex flex-col justify-center border-b border-outline-variant shrink-0">
|
||||||
|
<h1 class="text-[24px] font-bold leading-tight text-suit-black">CHALLENGE MODE</h1>
|
||||||
|
<p class="text-[12px] text-[#a0a0a0] mt-1">Curated puzzles · Beat par for bonus XP</p>
|
||||||
|
</header>
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="mx-margin-edge mt-4 bg-surface-container rounded-[4px] p-3 flex items-center justify-between border border-outline-variant shrink-0">
|
||||||
|
<div class="flex items-baseline gap-1">
|
||||||
|
<span class="text-[14px] font-bold text-suit-black">DONE 8/24</span>
|
||||||
|
<span class="text-[14px] font-bold text-highlight-celebration">(33%)</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-outline-variant text-[14px]">│</span>
|
||||||
|
<div class="text-[14px] font-bold text-suit-black">BEST AVG 03:42</div>
|
||||||
|
<span class="text-outline-variant text-[14px]">│</span>
|
||||||
|
<div class="text-[14px] font-bold text-highlight-valid">+1240 XP</div>
|
||||||
|
</div>
|
||||||
|
<!-- Scrollable List Area -->
|
||||||
|
<div class="flex-1 overflow-y-auto px-margin-edge pt-4 space-y-3 pb-6">
|
||||||
|
<!-- Card 1 -->
|
||||||
|
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
|
||||||
|
<div class="w-[6px] h-full bg-warning"></div>
|
||||||
|
<div class="flex-1 flex items-center justify-between px-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[14px] font-bold text-suit-black">DEEP STACK</span>
|
||||||
|
<span class="text-[12px] text-on-surface-variant">Win with 0 stock · ★★★☆☆</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-highlight-valid px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||||
|
✓ DONE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Card 2 -->
|
||||||
|
<div class="h-[80px] bg-surface-container border border-primary rounded-[4px] flex relative overflow-hidden">
|
||||||
|
<div class="w-[6px] h-full bg-highlight-valid"></div>
|
||||||
|
<div class="flex-1 flex items-center justify-between px-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[14px] font-bold text-suit-black">SPEED RUN</span>
|
||||||
|
<span class="text-[12px] text-on-surface-variant">Win under 2:30 · ★★☆☆☆</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-primary px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||||
|
▶ ACTIVE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Card 3 -->
|
||||||
|
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
|
||||||
|
<div class="w-[6px] h-full bg-suit-red"></div>
|
||||||
|
<div class="flex-1 flex items-center justify-between px-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[14px] font-bold text-suit-black">NO UNDO</span>
|
||||||
|
<span class="text-[12px] text-on-surface-variant">Win without undo · ★★★★☆</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-primary px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||||
|
▶ ACTIVE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Card 4 -->
|
||||||
|
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
|
||||||
|
<div class="w-[6px] h-full bg-info"></div>
|
||||||
|
<div class="flex-1 flex items-center justify-between px-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[14px] font-bold text-suit-black">FOUR SUITS</span>
|
||||||
|
<span class="text-[12px] text-on-surface-variant">1 card per suit · ★☆☆☆☆</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-highlight-valid px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||||
|
✓ DONE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Card 5 (Locked) -->
|
||||||
|
<div class="h-[80px] bg-surface-container border border-outline-variant rounded-[4px] flex relative overflow-hidden opacity-60">
|
||||||
|
<div class="w-[6px] h-full bg-highlight-celebration"></div>
|
||||||
|
<div class="flex-1 flex items-center justify-between px-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[14px] font-bold text-suit-black">PERFECT RUN</span>
|
||||||
|
<span class="text-[12px] text-on-surface-variant">Below par moves · ★★★★★</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-outline px-2 py-1 rounded-[2px] text-on-surface text-[11px] font-bold">
|
||||||
|
🔒 LOCKED
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Filler Graphic for retro feel -->
|
||||||
|
<div class="flex items-center justify-center py-4">
|
||||||
|
<div class="h-[1px] flex-1 bg-outline-variant"></div>
|
||||||
|
<span class="px-4 text-[10px] text-outline text-label-caps">END OF LIST</span>
|
||||||
|
<div class="h-[1px] flex-1 bg-outline-variant"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Shared Component: Terminal Context (Used as Footer) -->
|
||||||
|
<div class="h-[24px] bg-surface px-4 flex items-center justify-between text-[10px] font-mono border-t border-outline-variant shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-primary">▌ NORMAL</span>
|
||||||
|
<span class="text-outline">│</span>
|
||||||
|
<span class="text-on-surface-variant uppercase tracking-widest">challenge</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-[#a0a0a0] flex items-center gap-3">
|
||||||
|
<span>[ENTER] select</span>
|
||||||
|
<span>[F] filter</span>
|
||||||
|
<span class="text-suit-red">[ESC] back</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Retro Scanline Overlay -->
|
||||||
|
<div class="absolute inset-0 retro-scanline z-50"></div>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,258 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=390, height=844, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>Rusty Solitaire - Daily Challenge</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500;600&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #101417;
|
||||||
|
color: #e0e3e6;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.scanline-bg {
|
||||||
|
background: linear-gradient(to bottom, transparent 50%, rgba(26, 26, 26, 0.5) 50%);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"surface": "#151515",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"outline": "#505050",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"highlight-celebration": "#e1a3ee"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"touch-target-min": "48dp"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"headline": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col min-h-screen max-w-[390px] mx-auto overflow-hidden shadow-2xl border-x border-outline">
|
||||||
|
<!-- 1. Status Bar -->
|
||||||
|
<div class="h-[32px] bg-[#202020] flex items-center justify-between px-margin-edge border-b border-outline">
|
||||||
|
<span class="font-hud-timer text-[12px] text-on-surface-variant">▌daily/2024-127.json</span>
|
||||||
|
<div class="bg-warning/10 border border-warning px-2 py-0.5 rounded-sm">
|
||||||
|
<span class="font-hud-timer text-[11px] text-warning font-bold tracking-tighter">EXPIRES 11:42:30</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Main Content Canvas -->
|
||||||
|
<main class="flex-1 p-margin-edge space-y-4 overflow-y-auto pb-8">
|
||||||
|
<!-- 2. Header Card -->
|
||||||
|
<section class="h-[130px] bg-[#1a1a1a] border border-[#353535] rounded-lg p-4 flex flex-col justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-headline font-bold text-[24px] text-suit-black leading-none">MAY 07 · 2026</span>
|
||||||
|
<span class="font-headline font-extrabold text-[32px] text-highlight-valid -tracking-[0.01em] leading-tight">#2024-127</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-label-caps text-[11px] text-on-surface-variant/70">DRAW-3 · DIFFICULTY ★★★☆☆ · PAR 04:30</span>
|
||||||
|
</section>
|
||||||
|
<!-- 3. Primary CTA -->
|
||||||
|
<button class="w-full h-[64px] bg-primary-container text-surface font-headline font-bold text-[14px] uppercase tracking-wider rounded-lg active:scale-95 transition-transform duration-80 flex items-center justify-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-[18px]">play_arrow</span>
|
||||||
|
ATTEMPT TODAY'S SEED
|
||||||
|
</button>
|
||||||
|
<!-- 4. Your Attempts Card -->
|
||||||
|
<section class="h-[96px] bg-[#202020] rounded-lg p-4 flex flex-col justify-between">
|
||||||
|
<span class="font-label-caps text-[11px] text-on-surface-variant/60 uppercase">YOUR ATTEMPTS</span>
|
||||||
|
<div class="flex justify-between items-end">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-hud-score text-[16px] text-suit-black">BEST 04:12</span>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<span class="bg-warning text-surface text-[10px] font-bold px-1.5 py-0.5 rounded-sm">WIN</span>
|
||||||
|
<span class="font-label-caps text-[11px] text-warning">RANK 17/2843</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="font-hud-timer text-[13px] text-error mb-1">LAST: FAILED at move 47</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- 5. Leaderboard Card -->
|
||||||
|
<section class="bg-[#202020] rounded-lg p-4 flex flex-col flex-grow">
|
||||||
|
<span class="font-label-caps text-[11px] text-on-surface-variant/60 uppercase mb-4">TOP TODAY · 2,843 PLAYERS</span>
|
||||||
|
<div class="space-y-0 divide-y divide-[#353535]">
|
||||||
|
<!-- Row 1 -->
|
||||||
|
<div class="h-[32px] flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center bg-warning text-surface text-[10px] font-bold rounded-full">01</span>
|
||||||
|
<span class="font-hud-timer text-[14px]">swift_jaguar</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-hud-timer text-[14px] text-on-surface-variant">02:47</span>
|
||||||
|
</div>
|
||||||
|
<!-- Row 2 -->
|
||||||
|
<div class="h-[32px] flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center bg-[#a0a0a0] text-surface text-[10px] font-bold rounded-full">02</span>
|
||||||
|
<span class="font-hud-timer text-[14px]">base16_fan</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-hud-timer text-[14px] text-on-surface-variant">03:12</span>
|
||||||
|
</div>
|
||||||
|
<!-- Row 3 -->
|
||||||
|
<div class="h-[32px] flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center bg-[#7a5d3b] text-surface text-[10px] font-bold rounded-full">03</span>
|
||||||
|
<span class="font-hud-timer text-[14px]">cli_player</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-hud-timer text-[14px] text-on-surface-variant">03:54</span>
|
||||||
|
</div>
|
||||||
|
<!-- Row 4 -->
|
||||||
|
<div class="h-[32px] flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center bg-[#353535] text-on-surface-variant text-[10px] font-bold rounded-full">04</span>
|
||||||
|
<span class="font-hud-timer text-[14px]">tablejockey</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-hud-timer text-[14px] text-on-surface-variant">04:01</span>
|
||||||
|
</div>
|
||||||
|
<!-- Row 5 -->
|
||||||
|
<div class="h-[32px] flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center bg-[#353535] text-on-surface-variant text-[10px] font-bold rounded-full">05</span>
|
||||||
|
<span class="font-hud-timer text-[14px]">vim_motions</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-hud-timer text-[14px] text-on-surface-variant">04:05</span>
|
||||||
|
</div>
|
||||||
|
<!-- Row 17 (YOU) -->
|
||||||
|
<div class="h-[36px] flex items-center justify-between bg-primary-container/10 -mx-4 px-4 border-y border-primary-container/20">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center bg-primary-container text-surface text-[10px] font-bold rounded-full">17</span>
|
||||||
|
<span class="font-hud-timer text-[14px] text-primary-container font-bold">(YOU) anonymous</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-hud-timer text-[14px] text-primary-container font-bold">04:12</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex-1 border-t border-[#353535] pt-4 flex flex-col items-center justify-center opacity-30 select-none">
|
||||||
|
<span class="material-symbols-outlined text-[48px]">terminal</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-2">END OF VISIBLE LOG</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- 6. Footer Navigation -->
|
||||||
|
<footer class="h-[24px] bg-background border-t border-outline flex items-center justify-between px-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant">▌ NORMAL │ daily</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[ENTER]</span> attempt</span>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[L]</span> full leaderboard</span>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[ESC]</span> back</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Shared Component Shell Rendering Logic -->
|
||||||
|
<header class="w-full top-0 sticky bg-background border-b border-outline flex justify-between items-center px-margin-edge h-action-bar-height hidden">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-primary">terminal</span>
|
||||||
|
<h1 class="font-headline text-headline text-primary uppercase tracking-widest">RUSTY SOLITAIRE</h1>
|
||||||
|
</div>
|
||||||
|
<span class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors duration-120 cursor-pointer">settings</span>
|
||||||
|
</header>
|
||||||
|
<nav class="fixed bottom-0 w-full h-action-bar-height z-50 bg-surface-container border-t border-outline flex justify-around items-center px-2 hidden">
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||||
|
<span class="material-symbols-outlined">refresh</span>
|
||||||
|
<span class="font-label-caps text-label-caps">DEAL</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||||
|
<span class="material-symbols-outlined">undo</span>
|
||||||
|
<span class="font-label-caps text-label-caps">UNDO</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||||
|
<span class="material-symbols-outlined">lightbulb</span>
|
||||||
|
<span class="font-label-caps text-label-caps">HINT</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-primary dark:text-primary-fixed-dim hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||||
|
<span class="material-symbols-outlined">menu</span>
|
||||||
|
<span class="font-label-caps text-label-caps">MENU</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,285 @@
|
|||||||
|
---
|
||||||
|
name: Terminal
|
||||||
|
colors:
|
||||||
|
surface: '#151515'
|
||||||
|
surface-dim: '#0d0d0d'
|
||||||
|
surface-bright: '#2a2a2a'
|
||||||
|
surface-container-lowest: '#0a0a0a'
|
||||||
|
surface-container-low: '#1a1a1a'
|
||||||
|
surface-container: '#202020'
|
||||||
|
surface-container-high: '#2a2a2a'
|
||||||
|
surface-container-highest: '#353535'
|
||||||
|
on-surface: '#d0d0d0'
|
||||||
|
on-surface-variant: '#a0a0a0'
|
||||||
|
inverse-surface: '#d0d0d0'
|
||||||
|
inverse-on-surface: '#151515'
|
||||||
|
outline: '#505050'
|
||||||
|
outline-variant: '#353535'
|
||||||
|
surface-tint: '#a54242'
|
||||||
|
primary: '#a54242'
|
||||||
|
on-primary: '#151515'
|
||||||
|
primary-container: '#3a1f1f'
|
||||||
|
on-primary-container: '#d5a8a8'
|
||||||
|
inverse-primary: '#993e3e'
|
||||||
|
secondary: '#acc267'
|
||||||
|
on-secondary: '#151515'
|
||||||
|
secondary-container: '#2a3320'
|
||||||
|
on-secondary-container: '#c5d585'
|
||||||
|
tertiary: '#e1a3ee'
|
||||||
|
on-tertiary: '#151515'
|
||||||
|
tertiary-container: '#3a2a40'
|
||||||
|
on-tertiary-container: '#eec3f5'
|
||||||
|
error: '#fb9fb1'
|
||||||
|
on-error: '#151515'
|
||||||
|
error-container: '#4a2530'
|
||||||
|
on-error-container: '#fdc3ce'
|
||||||
|
background: '#151515'
|
||||||
|
on-background: '#d0d0d0'
|
||||||
|
surface-variant: '#353535'
|
||||||
|
suit-red: '#fb9fb1'
|
||||||
|
suit-black: '#d0d0d0'
|
||||||
|
suit-red-cb: '#acc267'
|
||||||
|
highlight-valid: '#acc267'
|
||||||
|
highlight-celebration: '#e1a3ee'
|
||||||
|
highlight-warning: '#ddb26f'
|
||||||
|
highlight-info: '#12cfc0'
|
||||||
|
typography:
|
||||||
|
hud-score:
|
||||||
|
fontFamily: JetBrains Mono
|
||||||
|
fontSize: 24px
|
||||||
|
fontWeight: '700'
|
||||||
|
lineHeight: 32px
|
||||||
|
letterSpacing: '-0.02em'
|
||||||
|
hud-timer:
|
||||||
|
fontFamily: JetBrains Mono
|
||||||
|
fontSize: 16px
|
||||||
|
fontWeight: '400'
|
||||||
|
lineHeight: 24px
|
||||||
|
card-rank:
|
||||||
|
fontFamily: JetBrains Mono
|
||||||
|
fontSize: 18px
|
||||||
|
fontWeight: '700'
|
||||||
|
lineHeight: 18px
|
||||||
|
body-md:
|
||||||
|
fontFamily: Inter
|
||||||
|
fontSize: 16px
|
||||||
|
fontWeight: '400'
|
||||||
|
lineHeight: 24px
|
||||||
|
label-caps:
|
||||||
|
fontFamily: JetBrains Mono
|
||||||
|
fontSize: 12px
|
||||||
|
fontWeight: '500'
|
||||||
|
lineHeight: 16px
|
||||||
|
letterSpacing: '0.08em'
|
||||||
|
headline:
|
||||||
|
fontFamily: JetBrains Mono
|
||||||
|
fontSize: 28px
|
||||||
|
fontWeight: '700'
|
||||||
|
lineHeight: 32px
|
||||||
|
letterSpacing: '-0.01em'
|
||||||
|
rounded:
|
||||||
|
sm: 0.125rem
|
||||||
|
DEFAULT: 0.25rem
|
||||||
|
md: 0.5rem
|
||||||
|
lg: 0.75rem
|
||||||
|
xl: 1rem
|
||||||
|
full: 9999px
|
||||||
|
spacing:
|
||||||
|
margin-edge: 1rem
|
||||||
|
gutter-card: 0.375rem
|
||||||
|
stack-overlap: 2rem
|
||||||
|
touch-target-min: 48dp
|
||||||
|
---
|
||||||
|
|
||||||
|
## Brand & Style
|
||||||
|
|
||||||
|
The "Terminal" design system replaces the previous "Premium Solitaire" calm-indie aesthetic with a **retro-terminal / synthwave** identity. The intent is the visual confidence of a well-tuned terminal emulator (think Berkeley Mono dotfiles, base16-eighties, CRT phosphor): monospaced, dense, legible, snappy. It is *not* casino-glitz, *not* skeuomorphic felt, and *not* whimsical.
|
||||||
|
|
||||||
|
The personality is **technical, deliberate, slightly playful**. Cards are flat with thin colored strokes; the HUD reads like a status bar; modals look like terminal panes. Motion is short and snap-easing — no bouncy springs. Long-session calm is preserved by keeping the chroma low and reserving saturated accents for *meaning* (CTAs, feedback, celebrations) rather than decoration.
|
||||||
|
|
||||||
|
Influences: base16-eighties (Chris Kempson), Berkeley Mono, Vim/Neovim status lines, the iA Writer aesthetic, classic CRT phosphor with no chromatic aberration.
|
||||||
|
|
||||||
|
## Palette
|
||||||
|
|
||||||
|
The palette is base16-eighties — a 16-slot terminal palette where indices 00–07 form a monochrome ramp and 08–0F provide saturated accents. We map base16 slots to Material Design 3 token roles below.
|
||||||
|
|
||||||
|
### Source palette (base16-eighties)
|
||||||
|
|
||||||
|
| Slot | Hex | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| base00 | `#151515` | background |
|
||||||
|
| base01 | `#202020` | surface-container |
|
||||||
|
| base02 | `#303030` | line-highlight (subtle) |
|
||||||
|
| base03 | `#505050` | outline / muted text |
|
||||||
|
| base04 | `#b0b0b0` | secondary text |
|
||||||
|
| base05 | `#d0d0d0` | foreground / on-surface |
|
||||||
|
| base06 | `#e0e0e0` | bright text |
|
||||||
|
| base07 | `#f5f5f5` | brightest highlight |
|
||||||
|
| base08 | `#fb9fb1` | red — used for `error`, `suit-red` |
|
||||||
|
| base09 | `#ddb26f` | orange — used for warning chips |
|
||||||
|
| base0A | `#acc267` | yellow/lime — used for `highlight-valid` (drag targets, valid moves) |
|
||||||
|
| base0B | `#12cfc0` | green/teal — used for `highlight-info` (toasts, neutral status) |
|
||||||
|
| base0C | `#6fc2ef` | cyan/sky — historically the primary CTA; now reserved for ad-hoc accents only |
|
||||||
|
| base0D | `#6fc2ef` | (alias) |
|
||||||
|
| base08 (project) | `#a54242` | brick red — primary CTA, focus ring, `selection` (project-specific extension; the base16-eighties `base08` slot is `#fb9fb1` pink which we keep as `error`/`suit-red`) |
|
||||||
|
| `suit-red-cb` slot | `#acc267` | lime — color-blind-mode swap for red suits (was `#6fc2ef` cyan before the 2026-05-08 primary-accent swap; lime is the next-best non-red base16-eighties accent) |
|
||||||
|
| base0E | `#e1a3ee` | violet — used for celebration (level-up, achievement unlock) |
|
||||||
|
| base0F | `#fb9fb1` | (alias) |
|
||||||
|
|
||||||
|
### Semantic assignments
|
||||||
|
|
||||||
|
- **CTA / Primary action**: brick red `#a54242`. Reserved for "Play," "New Game," "Save," "Resume," and the focus ring on selected cards. Never used decoratively. (Was cyan `#6fc2ef` before the 2026-05-08 swap.)
|
||||||
|
- **Valid-move / drag-target highlight**: lime `#acc267`. Reserved for in-game feedback only. Never appears in chrome.
|
||||||
|
- **Celebration**: lavender `#e1a3ee`. Used for level-up flashes, achievement unlock cards, and the daily-streak chip when the streak is active. Quiet otherwise.
|
||||||
|
- **Warning / soft alert**: gold `#ddb26f`. Used for "challenge expires in N minutes" chips, sync-pending status, and the daily-seed countdown.
|
||||||
|
- **Info**: teal `#12cfc0`. Used for neutral system toasts and the sync-connected indicator.
|
||||||
|
- **Error**: pink `#fb9fb1`. Used for sync conflict, server unreachable, invalid move shake.
|
||||||
|
|
||||||
|
## Suit Colors
|
||||||
|
|
||||||
|
**Two-color traditional pairing**, with mandatory color-blind
|
||||||
|
support. Saturated red for hearts + diamonds, near-white for clubs
|
||||||
|
+ spades — the "Microsoft Solitaire on dark mode" feel of a real
|
||||||
|
playing-card deck. (A brief 4-color-deck experiment shipped between
|
||||||
|
v0.21.0 and the next post-cut commit; reverted to traditional
|
||||||
|
2-color at the player's request.)
|
||||||
|
|
||||||
|
| Suit | Default | Color-blind mode | Glyph differentiation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Hearts | `#e35353` (saturated red) | `#acc267` (lime) | Solid filled glyph |
|
||||||
|
| Diamonds | `#e35353` (saturated red) | `#acc267` (lime) | **Outlined glyph (1.5px stroke)** |
|
||||||
|
| Spades | `#e8e8e8` (near-white) | `#e8e8e8` (unchanged) | Solid filled glyph |
|
||||||
|
| Clubs | `#e8e8e8` (near-white) | `#e8e8e8` (unchanged) | **Outlined glyph (1.5px stroke)** |
|
||||||
|
|
||||||
|
The outlined-glyph treatment is the **primary** differentiation mechanism. Color is supplementary. This means a player viewing the game on a monochrome display, or with severe red-green deficiency, can still distinguish all four suits without context. This is a hard requirement, not an optional setting.
|
||||||
|
|
||||||
|
The "color-blind mode" toggle in Settings swaps both red suits (hearts + diamonds) from `#e35353` to `#acc267` (lime); clubs + spades stay at the near-white. The toggle does not turn the outlined glyphs on or off, because outlined glyphs are always on. (Was red→cyan before the 2026-05-08 primary-accent swap; CBM moved to lime to stay hue-distinct from the new red-family primary.)
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
**Monospace-forward, dual-font system.**
|
||||||
|
|
||||||
|
- **JetBrains Mono** is used for: HUD (score, timer, moves), card rank/value text, all labels, all headlines, all numerals anywhere in the app, and any chip-style component. This is the dominant face.
|
||||||
|
- **Inter** is used only for: long-form body copy (Help screen, Settings descriptions, achievement tooltips, onboarding copy). It is the *exception*, not the default.
|
||||||
|
|
||||||
|
Weights: 400 regular, 500 medium for labels, 700 bold for HUD numbers and headlines. No 600 / no italics anywhere — the terminal aesthetic doesn't have them.
|
||||||
|
|
||||||
|
Letter spacing: tight (`-0.02em`) on HUD score for visual mass; wide (`+0.08em`) on uppercase labels for readability at 12px. Body uses default (0).
|
||||||
|
|
||||||
|
HUD numbers must use **tabular figures** (`font-feature-settings: 'tnum'`) so the timer and score don't reflow as digits change.
|
||||||
|
|
||||||
|
## Layout & Spacing
|
||||||
|
|
||||||
|
Optimized for **Android portrait, 390×844 (Pixel 6 baseline), API 34**.
|
||||||
|
|
||||||
|
- **Margins**: 16px (1rem) edge safety margin. *Tighter than the previous system's 24px.* Eighties palettes are dense by nature; over-padding fights the aesthetic.
|
||||||
|
- **Tableau**: 7-column layout, 32px (2rem) vertical card overlap. Tighter than before to fit a longer cascade on phone screens.
|
||||||
|
- **HUD position**: top of screen, in the system safe area. Bottom 64px holds the action bar (Undo / Hint / New Game / Auto-complete). Action bar is **always visible** in-game — no hover-fade — because there is no hover on touch.
|
||||||
|
- **Touch target minimum**: 48dp on all interactive elements. Cards in the tableau may be smaller visually but use a 48dp invisible hit area centered on the visible glyph.
|
||||||
|
|
||||||
|
## Elevation & Depth
|
||||||
|
|
||||||
|
Depth is created through **tonal layering and 1px outlines**, not blur shadows. (Synthwave-flat, not Material-soft.)
|
||||||
|
|
||||||
|
- **Level 0 (Background)**: the `#151515` base canvas.
|
||||||
|
- **Level 1 (Tableau slots, empty piles)**: 1px dashed outline in `#353535`. Empty foundations show a faint suit glyph at 12% opacity inside the outline.
|
||||||
|
- **Level 2 (Cards at rest)**: solid `#1a1a1a` fill, 1px solid border in the suit color (so the suit is detectable at a glance even if the card is partially obscured).
|
||||||
|
- **Level 3 (Active / dragged card)**: same border, but glow effect: 0 0 12px of `#a54242` at 40% opacity. **No scale transform** — flatness preserved. Z-index lifts above siblings.
|
||||||
|
- **Modals**: full-screen with backdrop `#151515` at 95% opacity (just enough to dim the table without blurring it). Modal panel is `#202020` with a 1px `#505050` border — like a terminal pane.
|
||||||
|
- **Toasts**: bottom of screen, `#202020` fill, 1px border in the toast's accent color (info=teal, warning=gold, error=pink, celebration=lavender). 16px monospaced caption.
|
||||||
|
|
||||||
|
No `box-shadow` is used anywhere. **All depth is achieved with borders and tonal value.** This is a hard constraint.
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
The shape language is **soft-rounded but tight**:
|
||||||
|
|
||||||
|
- **Cards**: `rounded-md` (8px) — slightly less rounded than the previous system's 16px to read more "technical."
|
||||||
|
- **Buttons / chips / inputs**: `rounded` (4px) default, `rounded-sm` (2px) for the smallest chips.
|
||||||
|
- **Modals / sheets**: `rounded-lg` (12px).
|
||||||
|
- **Avatars / circular indicators**: `rounded-full`.
|
||||||
|
- **Card-back pattern corners**: matches the card's `rounded-md`.
|
||||||
|
|
||||||
|
Selection highlights use a **2px inset stroke** in `#a54242` following the host shape's corner radius. Never an outer stroke — the outer stroke is reserved for the suit-color hairline.
|
||||||
|
|
||||||
|
## Motion
|
||||||
|
|
||||||
|
**Snappy, no spring.** All transitions use `ease-out` with a 120ms duration unless specified.
|
||||||
|
|
||||||
|
- Card lift (start drag): 80ms.
|
||||||
|
- Card place (drop): 120ms with a 16ms holdframe (no bounce).
|
||||||
|
- Modal enter: 200ms ease-out, fade + 8px translate-up.
|
||||||
|
- Modal exit: 120ms ease-in, fade only.
|
||||||
|
- Selection ring appear: 80ms.
|
||||||
|
- Win-summary stat reveal: 60ms each, staggered 40ms.
|
||||||
|
- HUD number tick: instant (no transition) — terminal counters don't ease.
|
||||||
|
|
||||||
|
**Optional CRT effect**: a 1-frame scanline sweep across the screen on game-state transitions (start, win, restart). User-toggleable in Settings. Off by default.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Game Cards
|
||||||
|
|
||||||
|
Flat face design.
|
||||||
|
- Background: `#1a1a1a`
|
||||||
|
- Border: none. The card shape is defined by the body fill alone against the play surface. The earlier 1px suit-coloured border was removed because it produced visible anti-aliasing artifacts at the rounded corners (a "gray sliver" where the colored stroke faded through gray pixels into the dark play surface). The 5-unit brightness gap between `#1a1a1a` body and `#151515` surface is enough to read as a card edge without an explicit stroke.
|
||||||
|
- Top-left: rank in JetBrains Mono Bold 18px + small suit glyph (10px)
|
||||||
|
- Bottom-right: large suit glyph (32px), upright (same orientation as the top-left small glyph — single-orientation digital play does not benefit from the traditional 180° inverted-corner indicator)
|
||||||
|
- Corner radius: 8px
|
||||||
|
- Suit differentiation: hearts and spades have **filled** glyphs; diamonds and clubs have **outlined** glyphs (1.5px stroke)
|
||||||
|
|
||||||
|
### Card Back ("Terminal" theme)
|
||||||
|
|
||||||
|
- Theme name: `"Terminal"`
|
||||||
|
- Author: `"Rusty Solitaire"`
|
||||||
|
- Background: `#151515`
|
||||||
|
- Pattern: horizontal scanlines at 2px pitch in `#1a1a1a` (1px line, 1px gap), full bleed
|
||||||
|
- Border: 1px solid `#353535`
|
||||||
|
- Top-left badge: a 12×16px solid `#a54242` block (the "terminal cursor"), 6px from the corner
|
||||||
|
- Bottom-right monogram: the characters `▌RS` in JetBrains Mono 12px, color `#505050`, 6px from the corner
|
||||||
|
- Corner radius: 8px (matches face)
|
||||||
|
|
||||||
|
### Primary Buttons
|
||||||
|
|
||||||
|
Solid `#a54242` fill, `#151515` text, JetBrains Mono Medium 14px uppercase with `+0.08em` tracking. 4px corner radius. Pressed state: darken to `#7a3030`. Disabled: `#353535` fill, `#505050` text.
|
||||||
|
|
||||||
|
### Secondary Buttons
|
||||||
|
|
||||||
|
Transparent fill, 1px `#505050` border, `#d0d0d0` text. Hover/press: border becomes `#a54242`, text becomes `#a54242`.
|
||||||
|
|
||||||
|
### HUD Chips
|
||||||
|
|
||||||
|
`#202020` fill, no border, 4px radius. Monospaced 16px text. Score chip pulses to `#acc267` for 200ms when score increases.
|
||||||
|
|
||||||
|
### Drag Targets
|
||||||
|
|
||||||
|
When a card is being dragged over a valid pile, the pile's empty-slot dashed outline becomes:
|
||||||
|
- Solid 1px in `#acc267`
|
||||||
|
- Plus a 0 0 8px outer glow in `#acc267` at 30% opacity
|
||||||
|
|
||||||
|
This is the *only* place glow effects appear in the system.
|
||||||
|
|
||||||
|
### Modals
|
||||||
|
|
||||||
|
Full-screen backdrop at 95% opacity. Centered panel: `#202020` fill, 1px `#505050` border, 12px corner radius. Title bar shows the screen name in monospaced 14px, color `#a0a0a0`, with a single `▌` cursor character prefix to reinforce the terminal pane motif.
|
||||||
|
|
||||||
|
### Navigation Bar
|
||||||
|
|
||||||
|
Fixed at the bottom of in-game screens. Height: 64px. `#202020` fill, 1px top border in `#353535`. Four icon buttons: Undo / Hint / New / Auto-complete. Icons: 24px, 1.5px stroke weight, color `#d0d0d0`. Active/pressed: icon color `#a54242`.
|
||||||
|
|
||||||
|
### Status / Sync Indicator
|
||||||
|
|
||||||
|
Top-right corner of the HUD: a 6px circular dot.
|
||||||
|
- Connected & synced: `#12cfc0`
|
||||||
|
- Pending: `#ddb26f` (pulsing 1.5s)
|
||||||
|
- Error: `#fb9fb1` (steady)
|
||||||
|
- Offline: `#505050`
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
1. **Color-blind mode** (Settings → Gameplay): swaps the red suits' default `#e35353` for `#acc267` (lime). Outlined-glyph differentiation remains active in *all* modes.
|
||||||
|
2. **High-contrast mode** (Settings → Gameplay): boosts on-surface from `#d0d0d0` to `#f5f5f5`, outline from `#505050` to `#a0a0a0`, suit-red from `#fb9fb1` to `#ff8aa0`.
|
||||||
|
3. **Reduce-motion mode** (Settings → Gameplay): disables card-lift transition (instant z-lift), disables CRT scanline effect, disables the warning-chip pulse animation.
|
||||||
|
4. **Tabular figures** are mandatory for any number that updates live (timer, score, moves) so they don't reflow.
|
||||||
|
5. **Touch targets** are 48dp minimum even when the visual element is smaller.
|
||||||
|
6. **Text contrast**: all body text on background passes WCAG AA at minimum (`#d0d0d0` on `#151515` = 9.5:1; `#a0a0a0` on `#151515` = 5.7:1).
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# Terminal — Desktop Adaptation Spec
|
||||||
|
|
||||||
|
> **Why this exists.** The 24 mockups in this directory are mobile
|
||||||
|
> (390 × 844 logical, iPhone 14 Pro frame) with one exception
|
||||||
|
> (`home-menu-desktop.html`). The Stitch project that produced them
|
||||||
|
> is named "Solitaire Quest *Mobile* Redesign" — the mobile-first
|
||||||
|
> framing was deliberate when the new Android target opened, but
|
||||||
|
> desktop is still the primary delivery surface. Porting the mobile
|
||||||
|
> mockups 1:1 would land a 390-px-wide column floating in the middle
|
||||||
|
> of an 1800 × 1100 window. This file is the rules-based desktop
|
||||||
|
> companion — apply these adaptations whenever you port a Bevy
|
||||||
|
> plugin against a mobile mockup in this directory.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
* **Token system.** All tokens (palette, type scale, spacing,
|
||||||
|
radii, motion) in `design-system.md` are layout-agnostic and
|
||||||
|
apply unchanged on both targets. Do **not** introduce desktop-
|
||||||
|
specific token variants — adapt geometry, not tokens.
|
||||||
|
* **Already adapted in code.** v0.20.0's port is layout-agnostic
|
||||||
|
(modal scaffold, toasts, table chrome, card chrome, gameplay-
|
||||||
|
feedback, splash cursor). Those surfaces already adapt
|
||||||
|
correctly because their Bevy UI nodes use flex / percent /
|
||||||
|
stretch sizing rather than fixed pixel widths from the
|
||||||
|
mockups.
|
||||||
|
* **Not yet adapted in code.** Any future plugin port that
|
||||||
|
copies layout from a mobile mockup must apply the rules below.
|
||||||
|
|
||||||
|
## Viewport assumptions
|
||||||
|
|
||||||
|
| Range | Width × height | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Mobile target | 390 × 844 | iPhone 14 Pro logical, Stitch mockup canvas |
|
||||||
|
| Desktop minimum | 1024 × 600 | Smaller windows degrade to mobile rules |
|
||||||
|
| Desktop default | ~70 % of monitor | `apply_smart_default_window_size` (since v0.19.0) |
|
||||||
|
| Desktop typical | 1600 × 900 to 2560 × 1440 | The range we tune for |
|
||||||
|
| Desktop max | 3840 × 2160 | 4K, with HiDPI scaling already applied |
|
||||||
|
|
||||||
|
The "smart default" sizer means a 1080p monitor opens a ~1344 × 756
|
||||||
|
window, a 1440p monitor opens ~1792 × 1008, a 4K monitor opens
|
||||||
|
~2688 × 1512. Tune for the 1600–2400 width band as the centre of
|
||||||
|
the distribution; below 1024 width, fall back to the mobile rules
|
||||||
|
verbatim.
|
||||||
|
|
||||||
|
## Universal adaptation rules
|
||||||
|
|
||||||
|
Apply these to every screen unless the per-screen section
|
||||||
|
overrides them.
|
||||||
|
|
||||||
|
### 1. Edge margins
|
||||||
|
|
||||||
|
| Mobile | Desktop |
|
||||||
|
|---|---|
|
||||||
|
| `margin-edge: 16px` (`SPACE_4`) | `SPACE_5` (24 px) for windows < 1440 wide; `SPACE_6` (32 px) for 1440–2400; `SPACE_7` (48 px) for ≥ 2400 |
|
||||||
|
|
||||||
|
Engine: drive from `LayoutResource` based on `Window` size, not a
|
||||||
|
constant.
|
||||||
|
|
||||||
|
### 2. Modal max-width
|
||||||
|
|
||||||
|
| Mobile | Desktop |
|
||||||
|
|---|---|
|
||||||
|
| `100% - 2 × edge-margin` | `min(720 px, 50 % of viewport)` |
|
||||||
|
|
||||||
|
The 720 px cap is already in `ui_modal::spawn_modal`. No code
|
||||||
|
change needed; this rule documents *why* it's there.
|
||||||
|
|
||||||
|
### 3. Vertical content stacks
|
||||||
|
|
||||||
|
A mobile screen often stacks `Header → Body → Footer` vertically
|
||||||
|
to fit a tall narrow column. On desktop, prefer horizontal
|
||||||
|
distribution where the content allows:
|
||||||
|
|
||||||
|
* **Header rows that stack vertically on mobile** (title above
|
||||||
|
count above timer) → keep them in one horizontal row on
|
||||||
|
desktop.
|
||||||
|
* **Two-column flex layouts** (e.g. Settings rows: label left,
|
||||||
|
control right) — already work on both targets; no change.
|
||||||
|
* **Cards stacking with `mt-48`-style fixed gaps** — replace with
|
||||||
|
flex / percent gaps so the layout breathes.
|
||||||
|
|
||||||
|
### 4. Touch-target minimums
|
||||||
|
|
||||||
|
Mobile spec mandates 48 dp minimum touch targets. Desktop has no
|
||||||
|
such floor (mouse precision is finer), but **don't shrink below
|
||||||
|
mobile's 48 px** for primary actions — keyboard / gamepad focus
|
||||||
|
rings still need a visible target.
|
||||||
|
|
||||||
|
Secondary controls (chip-style toggles, hotkey hints, etc.) can
|
||||||
|
shrink to `TYPE_BODY` (14 px) text + `SPACE_3` (12 px) padding on
|
||||||
|
desktop where they were larger on mobile.
|
||||||
|
|
||||||
|
### 5. Bottom-anchored elements
|
||||||
|
|
||||||
|
Mobile mockups often anchor key controls (action bar, primary CTA,
|
||||||
|
toast position) to the bottom of the viewport for thumb reach.
|
||||||
|
Desktop has no thumb-reach concern:
|
||||||
|
|
||||||
|
* **Toasts** — keep bottom-anchored (already done in `a137607`),
|
||||||
|
the design language is consistent across targets and the
|
||||||
|
bottom is still the least-disruptive overlay zone.
|
||||||
|
* **Action bars** — top of viewport on desktop unless the
|
||||||
|
per-screen section says otherwise. The HUD already sits on
|
||||||
|
top.
|
||||||
|
* **Single primary CTA** — modals already right-align in the
|
||||||
|
actions row; no change.
|
||||||
|
|
||||||
|
### 6. Typography rungs unchanged
|
||||||
|
|
||||||
|
Do **not** shift `TYPE_*` tokens up a rung for desktop. The
|
||||||
|
spec's 14 / 18 / 26 / 40 progression is already calibrated for
|
||||||
|
the desktop reading distance (60–90 cm). Mobile uses the same
|
||||||
|
rungs at a closer reading distance (30–40 cm); same physical
|
||||||
|
angular size on the eye.
|
||||||
|
|
||||||
|
### 7. Hotkey hints become full strings
|
||||||
|
|
||||||
|
Mobile cells like `▌Esc` — the cursor block plus key letter — can
|
||||||
|
expand to `[Esc] cancel` style on desktop where horizontal
|
||||||
|
real-estate is cheap. Drives discoverability of keyboard-only
|
||||||
|
flows. Optional; only apply where horizontal space exists.
|
||||||
|
|
||||||
|
## Per-screen adaptation rules
|
||||||
|
|
||||||
|
### Game Table
|
||||||
|
|
||||||
|
Mockup: `game-table-mobile.html` (390 × 844).
|
||||||
|
|
||||||
|
| Element | Mobile | Desktop |
|
||||||
|
|---|---|---|
|
||||||
|
| HUD band | full width, 56 px tall | full width, 48 px tall |
|
||||||
|
| Foundation row | 4 piles centred, fan-tight | 4 piles centred, **gutter doubled** so the row fills ~50 % of viewport width |
|
||||||
|
| Stock + waste | left of foundations, stacked | left of foundations, **horizontal pair**: stock on the left, waste to its immediate right (the mobile vertical pair feels cramped on a wide canvas) |
|
||||||
|
| Tableau row | 7 columns, 4 % gutter | 7 columns, **6 % gutter**, total tableau block ≤ 70 % viewport width |
|
||||||
|
| Card aspect | 2 : 3 (already in `Layout::card_size`) | unchanged — card aspect is domain |
|
||||||
|
| Tableau fan | `TABLEAU_FAN_FRAC = 0.25` | unchanged — fan is in card-height units, not viewport units |
|
||||||
|
| Drag-shadow offset | small | unchanged — pinned to 0 alpha under Terminal anyway |
|
||||||
|
|
||||||
|
**Engine impact:** `solitaire_engine/src/layout.rs::compute_layout`
|
||||||
|
already drives most of this from `Window::size()`. The mobile vs.
|
||||||
|
desktop difference is the gutter percentages — bake desktop
|
||||||
|
gutters when window width ≥ 1024.
|
||||||
|
|
||||||
|
### Win Summary
|
||||||
|
|
||||||
|
Mockup: `win-summary-mobile.html` (390 × 858).
|
||||||
|
|
||||||
|
| Element | Mobile | Desktop |
|
||||||
|
|---|---|---|
|
||||||
|
| Modal width | 100 % − 2 × edge | **`min(720 px, 50 % viewport)`** (already done by `ui_modal`) |
|
||||||
|
| Score row | stacked vertically (line per metric) | **3-column grid**: Score / Time / Moves in one row, breakdown rows below in single-line per row |
|
||||||
|
| Action buttons | full-width stacked (Play Again, Continue, Stats) | **right-aligned action row** — the existing `spawn_modal_actions` already does this on both targets |
|
||||||
|
|
||||||
|
**Engine impact:** `solitaire_engine/src/win_summary_plugin.rs`. The
|
||||||
|
score-breakdown-stagger animation (`MOTION_SCORE_BREAKDOWN_*`) is
|
||||||
|
unchanged across targets.
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
Mockup: `settings-mobile.html` (390 × 4330 — long scroll).
|
||||||
|
|
||||||
|
| Element | Mobile | Desktop |
|
||||||
|
|---|---|---|
|
||||||
|
| Modal width | 100 % − 2 × edge | `min(720 px, 50 % viewport)` |
|
||||||
|
| Sections | full-width labels above stacked controls | **section labels left, control widget right** — already the engine's pattern; no change |
|
||||||
|
| Long page | scroll the whole modal | **two-column layout**: nav (sections list) on left ~30 %, current section on right ~70 %. Reduces scroll distance on desktop |
|
||||||
|
| Sliders | full-width on mobile | cap at 320 px on desktop |
|
||||||
|
|
||||||
|
**Engine impact:** if a desktop port wants the two-column nav, it's
|
||||||
|
a `settings_plugin` rewrite. Keep the existing single-column
|
||||||
|
stacked-modal layout for now — it works on both targets and the
|
||||||
|
two-column variant is a polish item, not a blocker.
|
||||||
|
|
||||||
|
### Help & Controls
|
||||||
|
|
||||||
|
Mockup: `help-mobile.html` (390 × 2544).
|
||||||
|
|
||||||
|
| Element | Mobile | Desktop |
|
||||||
|
|---|---|---|
|
||||||
|
| Modal width | 100 % − 2 × edge | `min(720 px, 50 % viewport)` |
|
||||||
|
| Section list | one column of `Heading → 2-col rows` | **two columns of section blocks** for windows ≥ 1280 wide; halves vertical scroll distance |
|
||||||
|
| Hotkey rows | `key | description` 2-col flex | unchanged; 2-col already adapts |
|
||||||
|
|
||||||
|
**Engine impact:** `help_plugin`. Single-column on mobile, 2-col
|
||||||
|
on desktop windows ≥ 1280 wide is a flex-wrap option.
|
||||||
|
|
||||||
|
### Pause Menu
|
||||||
|
|
||||||
|
Mockup: `pause-menu-mobile.html` (390 × 1768).
|
||||||
|
|
||||||
|
Already a small modal; no significant geometry change. Modal
|
||||||
|
already uses `ui_modal::spawn_modal` which caps width and centres.
|
||||||
|
No desktop-specific rule.
|
||||||
|
|
||||||
|
### Home Menu
|
||||||
|
|
||||||
|
Mockup: `home-menu-mobile.html` and `home-menu-desktop.html`
|
||||||
|
(both already in this directory — desktop variant is the
|
||||||
|
authoritative reference).
|
||||||
|
|
||||||
|
The desktop mockup already specifies the layout. Cross-check it
|
||||||
|
against the mobile version when porting; differences are
|
||||||
|
deliberate (more horizontal real-estate, larger primary CTA, the
|
||||||
|
secondary actions row).
|
||||||
|
|
||||||
|
### Splash
|
||||||
|
|
||||||
|
Mockup: `splash-mobile.html` (390 × 844).
|
||||||
|
|
||||||
|
| Element | Mobile | Desktop |
|
||||||
|
|---|---|---|
|
||||||
|
| Full-screen overlay | `inset-0` | unchanged — splash always covers the viewport |
|
||||||
|
| Cursor block (`▌`) | 96 px JetBrains Mono | unchanged — already done in `cdcadda`. The 96 px size scales fine on desktop because the splash is a brand beat, not a layout-driven element |
|
||||||
|
| Title `RUSTY SOLITAIRE` | 32 px | scale to 40 px (`TYPE_DISPLAY`) on desktop |
|
||||||
|
| Subtitle `TERMINAL EDITION` | 12 px | unchanged |
|
||||||
|
| Boot log lines | 70 % width column | cap at 480 px so the column doesn't stretch on a wide window |
|
||||||
|
| Progress bar | 100 % − 2 × edge | cap at 720 px |
|
||||||
|
| Palette swatch row + version footer | bottom-anchored | unchanged; bottom-anchor still reads correctly on desktop |
|
||||||
|
|
||||||
|
**Engine impact:** `splash_plugin` already has the cursor block
|
||||||
|
(`cdcadda`). The boot log / progress bar / palette swatch rows
|
||||||
|
are the next polish increment when option D is picked up.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
Mockup: `stats-mobile.html` (390 × 2624).
|
||||||
|
|
||||||
|
| Element | Mobile | Desktop |
|
||||||
|
|---|---|---|
|
||||||
|
| Modal width | 100 % − 2 × edge | `min(720 px, 50 % viewport)` |
|
||||||
|
| Big-number cards | 2 × 2 grid | **4 × 1 row** for windows ≥ 1024 wide (the four headline metrics fit in a single horizontal row at desktop scale) |
|
||||||
|
| Latest-win caption | full-width line | unchanged |
|
||||||
|
| Replay clip / share row | full-width row | unchanged |
|
||||||
|
|
||||||
|
### Profile / Achievements / Theme Picker / Daily Challenge
|
||||||
|
|
||||||
|
These follow the **standard modal pattern** (`spawn_modal` with
|
||||||
|
header / body / actions). They already work on desktop because
|
||||||
|
`ui_modal` handles modal-width capping. Per-screen tweaks are
|
||||||
|
small and listed below; no structural changes:
|
||||||
|
|
||||||
|
* **Profile** — avatar + level / streak chips can flow into a
|
||||||
|
single horizontal row on desktop instead of stacking.
|
||||||
|
* **Achievements** — 3 × N grid on mobile becomes 4 × N or 5 × N
|
||||||
|
on desktop where windows ≥ 1280 wide.
|
||||||
|
* **Theme Picker** — 2-col grid of theme cards on mobile becomes
|
||||||
|
3- or 4-col on desktop.
|
||||||
|
* **Daily Challenge** — single-column scroll on both; no change.
|
||||||
|
|
||||||
|
## Mockup parity gap
|
||||||
|
|
||||||
|
The 9 missing-plugin screens (`splash`, `challenge`, `time-attack`,
|
||||||
|
`weekly-goals`, `leaderboard`, `sync`, `level-up`, `replay-overlay`,
|
||||||
|
`radial-menu`) have only mobile mockups. When porting any of these
|
||||||
|
plugins:
|
||||||
|
|
||||||
|
1. Read the mobile mockup for content + visual hierarchy.
|
||||||
|
2. Apply the universal adaptation rules above.
|
||||||
|
3. Apply the closest matching per-screen rule (e.g. an info modal
|
||||||
|
uses the same shape as Win Summary or Stats).
|
||||||
|
4. **No new layout pattern without explicit user approval.**
|
||||||
|
Adapting an existing pattern is in scope; inventing a desktop-
|
||||||
|
specific component is design work and should be flagged as such.
|
||||||
|
|
||||||
|
## Process notes
|
||||||
|
|
||||||
|
* **Smart-default sizer is the layout's source of truth.** Before
|
||||||
|
reading the mockup, always re-read `Window::size()` —
|
||||||
|
`apply_smart_default_window_size` runs at startup and the
|
||||||
|
player can resize freely. Hardcoded breakpoints in plugin code
|
||||||
|
should reference the *current* `Window` width via a
|
||||||
|
`LayoutResource` lookup, not the launch size.
|
||||||
|
* **`WindowResized` already drives layout recomputes** (CLAUDE.md
|
||||||
|
§3.4). Any per-window-width adaptation in this file should hook
|
||||||
|
into the existing recompute path, not a new system.
|
||||||
|
* **Mobile rules win at narrow desktop windows.** A user dragging
|
||||||
|
their desktop window down to 600 px width is closer to the
|
||||||
|
mobile use-case than the desktop one. Below 1024 px width,
|
||||||
|
apply the mobile rules verbatim.
|
||||||
|
* **Run on a 4K monitor before declaring a port done.** HiDPI
|
||||||
|
scaling routes through Bevy's logical sizing, but visual
|
||||||
|
polish (border thickness, motion budgets at high refresh rate)
|
||||||
|
is worth eyeballing.
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
.outlined-glyph {
|
||||||
|
-webkit-text-stroke: 1.5px currentColor;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.scanline-pattern {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
#1a1a1a,
|
||||||
|
#1a1a1a 2px,
|
||||||
|
#151515 2px,
|
||||||
|
#151515 4px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.tabular-nums {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"surface": "#151515",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-tertiary": "#293500",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"background": "#101417",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"suit-black": "#d0d0d0"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"touch-target-min": "48px",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"gutter-card": "0.375rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface text-on-surface font-body-md overflow-hidden selection:bg-primary selection:text-surface">
|
||||||
|
<!-- TopAppBar -->
|
||||||
|
<header class="fixed top-0 w-full flex justify-between items-center px-margin-edge h-[56px] bg-surface-container border-b border-outline dark:border-outline z-50">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-primary">terminal</span>
|
||||||
|
<h1 class="font-hud-score text-[18px] text-primary">solitaire.sh</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-[6px] h-[6px] rounded-full bg-info"></div>
|
||||||
|
<span class="material-symbols-outlined text-on-surface-variant">settings</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- HUD Band -->
|
||||||
|
<div class="fixed top-[56px] left-0 w-full h-[56px] bg-surface-container border-b border-outline-variant flex items-center justify-around px-margin-edge z-40">
|
||||||
|
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center">
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant">SCORE</span>
|
||||||
|
<span class="font-hud-score text-primary tabular-nums">247</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center border border-outline">
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant">TIME</span>
|
||||||
|
<span class="font-hud-timer text-on-surface tabular-nums">12:34</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center">
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant">MOVES</span>
|
||||||
|
<span class="font-hud-score text-secondary tabular-nums">87</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Main Game Table -->
|
||||||
|
<main class="pt-[124px] px-margin-edge h-screen w-full relative">
|
||||||
|
<!-- Top Row: Stock, Waste, Foundations -->
|
||||||
|
<div class="grid grid-cols-7 gap-gutter-card h-[110px]">
|
||||||
|
<!-- Stock -->
|
||||||
|
<div class="relative w-full h-full rounded-xl border border-outline-variant bg-surface overflow-hidden scanline-pattern">
|
||||||
|
<div class="absolute top-1 left-1 w-3 h-4 bg-suit-red-cb"></div>
|
||||||
|
<div class="absolute bottom-1 right-1 font-label-caps text-[8px] text-suit-black">▌RS</div>
|
||||||
|
<div class="absolute bottom-[-16px] left-0 w-full text-center font-label-caps text-[10px] text-on-surface-variant">STOCK · 18</div>
|
||||||
|
</div>
|
||||||
|
<!-- Waste -->
|
||||||
|
<div class="relative w-full h-full rounded-xl border border-suit-red bg-[#1a1a1a] flex flex-col justify-between p-1.5">
|
||||||
|
<div class="font-card-rank text-suit-red leading-none">10<br/><span class="font-normal">♥</span></div>
|
||||||
|
<div class="self-end text-[32px] font-card-rank text-suit-red rotate-180">♥</div>
|
||||||
|
</div>
|
||||||
|
<!-- Empty Gap -->
|
||||||
|
<div></div>
|
||||||
|
<!-- Foundation S -->
|
||||||
|
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
|
||||||
|
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank">♠</span>
|
||||||
|
</div>
|
||||||
|
<!-- Foundation H -->
|
||||||
|
<div class="relative w-full h-full rounded-xl border border-suit-red bg-[#1a1a1a] flex flex-col justify-between p-1.5">
|
||||||
|
<div class="font-card-rank text-suit-red leading-none">2<br/><span class="font-normal">♥</span></div>
|
||||||
|
<div class="self-end text-[32px] font-card-rank text-suit-red rotate-180">♥</div>
|
||||||
|
</div>
|
||||||
|
<!-- Foundation C -->
|
||||||
|
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
|
||||||
|
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank outlined-glyph">♣</span>
|
||||||
|
</div>
|
||||||
|
<!-- Foundation D -->
|
||||||
|
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
|
||||||
|
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank outlined-glyph">♦</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Tableau -->
|
||||||
|
<div class="mt-8 grid grid-cols-7 gap-gutter-card items-start relative h-[400px]">
|
||||||
|
<!-- Col 1 -->
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<div class="w-full h-[96px] rounded-xl border border-suit-black bg-[#1a1a1a] p-1.5">
|
||||||
|
<div class="font-card-rank text-suit-black leading-none">K<br/><span class="font-normal">♠</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Col 2 -->
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||||
|
<div class="absolute top-[32px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5">
|
||||||
|
<div class="font-card-rank text-suit-red leading-none">Q<br/><span class="font-normal">♥</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Col 3 -->
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||||
|
<div class="absolute top-[32px] w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||||
|
<div class="absolute top-[64px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5">
|
||||||
|
<div class="font-card-rank text-suit-red leading-none">10<br/><span class="font-normal outlined-glyph">♦</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Col 4 -->
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||||
|
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern absolute top-[32px]"></div>
|
||||||
|
<!-- Valid Drop Target Glow -->
|
||||||
|
<div class="absolute top-[64px] w-full h-[96px] rounded-xl border border-suit-black bg-[#1a1a1a] p-1.5 ring-4 ring-highlight-valid/30">
|
||||||
|
<div class="font-card-rank text-suit-black leading-none">9<br/><span class="font-normal">♠</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Col 5, 6 (Empty/Filler) -->
|
||||||
|
<div class="relative w-full"></div>
|
||||||
|
<div class="relative w-full"></div>
|
||||||
|
<!-- Col 7 -->
|
||||||
|
<div class="relative w-full">
|
||||||
|
<!-- Original Position Placeholder -->
|
||||||
|
<div class="w-full h-[96px] rounded-xl border border-dashed border-outline"></div>
|
||||||
|
<!-- Being Dragged Card -->
|
||||||
|
<div class="absolute top-[-20px] left-[30px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5 shadow-[0_0_20px_rgba(111,194,239,0.4)] z-50 ring-1 ring-primary/40">
|
||||||
|
<div class="font-card-rank text-suit-red leading-none">4<br/><span class="font-normal outlined-glyph">♦</span></div>
|
||||||
|
<div class="absolute bottom-1 right-1 text-[24px] font-card-rank text-suit-red rotate-180 outlined-glyph">♦</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- BottomNavBar / Action Bar -->
|
||||||
|
<nav class="fixed bottom-0 left-0 w-full h-action-bar-height bg-surface-container border-t border-outline-variant flex justify-around items-center px-margin-edge z-50">
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
|
||||||
|
<span class="material-symbols-outlined" data-icon="menu">menu</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[ESC] MENU</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-info font-bold active:opacity-80">
|
||||||
|
<span class="material-symbols-outlined" data-icon="undo">undo</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[U] UNDO</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
|
||||||
|
<span class="material-symbols-outlined" data-icon="lightbulb">lightbulb</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[H] HINT</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
|
||||||
|
<span class="material-symbols-outlined" data-icon="add_box">add_box</span>
|
||||||
|
<span class="font-label-caps text-[10px] mt-1">[N] NEW</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<!-- Drag & CRT Overlay (Visual Decoration) -->
|
||||||
|
<div class="pointer-events-none fixed inset-0 z-[100] opacity-[0.03] scanline-pattern mix-blend-overlay"></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,200 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"surface": "#151515"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"jetbrains": ["JetBrains Mono", "monospace"],
|
||||||
|
"inter": ["Inter", "sans-serif"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||||
|
body { background-color: #151515; }
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<!-- Mobile Container (390x844) -->
|
||||||
|
<div class="w-[390px] h-[844px] bg-surface flex flex-col overflow-hidden relative border border-outline/20">
|
||||||
|
<!-- 1. Status Bar -->
|
||||||
|
<header class="h-[32px] bg-surface-container flex items-center justify-between px-4 shrink-0">
|
||||||
|
<span class="font-jetbrains text-[12px] font-bold text-suit-black tracking-tight">▌rusty-solitaire(1) · MAN PAGE</span>
|
||||||
|
<button class="font-jetbrains text-[12px] font-bold text-suit-black/60 hover:text-primary transition-colors">× CLOSE</button>
|
||||||
|
</header>
|
||||||
|
<!-- 2. Heading Band -->
|
||||||
|
<div class="h-[120px] px-4 pt-10 pb-4 shrink-0">
|
||||||
|
<h1 class="font-jetbrains font-bold text-[24px] text-suit-black leading-none mb-1">GESTURES & SHORTCUTS</h1>
|
||||||
|
<p class="font-inter text-[13px] text-on-surface-variant/80">Touch gestures and keyboard equivalents.</p>
|
||||||
|
</div>
|
||||||
|
<!-- Scrollable Content Section -->
|
||||||
|
<main class="flex-1 overflow-y-auto px-4 pb-8 space-y-6">
|
||||||
|
<!-- 3a. TOUCH GESTURES -->
|
||||||
|
<section class="space-y-3">
|
||||||
|
<h2 class="font-jetbrains text-[11px] font-medium tracking-widest text-on-surface-variant/60 uppercase">TOUCH GESTURES</h2>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<!-- Row 1 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-suit-black" data-icon="square">square</span>
|
||||||
|
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">TAP card</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Select / unselect for move</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 2 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-suit-black" data-icon="east">east</span>
|
||||||
|
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">DRAG stack</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Move with translucent ghost preview</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 3 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-suit-black" data-icon="double_arrow">double_arrow</span>
|
||||||
|
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">DOUBLE-TAP</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Auto-send to best foundation</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 4 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-suit-black" data-icon="touch_app">touch_app</span>
|
||||||
|
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">LONG-PRESS</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Highlight all legal moves for card</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 5 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-suit-black" data-icon="south">south</span>
|
||||||
|
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">SWIPE DOWN</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Reveal hidden action bar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- 3b. KEYBOARD SHORTCUTS -->
|
||||||
|
<section class="space-y-3">
|
||||||
|
<h2 class="font-jetbrains text-[11px] font-medium tracking-widest text-on-surface-variant/60 uppercase">KEYBOARD SHORTCUTS</h2>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<!-- Row 1 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[U]</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Undo last move</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 2 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[H]</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Show hint</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 3 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[N]</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">New game</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 4 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[A]</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Auto-complete (when possible)</div>
|
||||||
|
</div>
|
||||||
|
<!-- Row 5 -->
|
||||||
|
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||||
|
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[ESC]</div>
|
||||||
|
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Pause / back</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- 4. Footer -->
|
||||||
|
<footer class="h-[24px] bg-surface-container border-t border-outline/20 flex items-center justify-between px-2 shrink-0">
|
||||||
|
<div class="font-jetbrains text-[10px] text-suit-black">
|
||||||
|
<span class="opacity-80">▌ NORMAL │ help</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-jetbrains text-[10px] uppercase tracking-wider flex items-center gap-1">
|
||||||
|
<span class="text-outline">PRESS</span>
|
||||||
|
<span class="text-on-surface-variant">[ESC]</span>
|
||||||
|
<span class="text-outline">OR TAP</span>
|
||||||
|
<span class="text-on-surface-variant">×</span>
|
||||||
|
<span class="text-outline">TO RETURN</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,343 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>RS_TERMINAL_OS - Rusty Solitaire</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #151515;
|
||||||
|
color: #d0d0d0;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.scanline {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: rgba(26, 26, 26, 0.5);
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #353535;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"surface": "#151515",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"outline": "#505050",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"suit-red": "#fb9fb1"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0px",
|
||||||
|
"lg": "0px",
|
||||||
|
"xl": "0px",
|
||||||
|
"full": "0px"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"touch-target-min": "48px",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"action-bar-height": "64px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"label-caps": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface text-on-surface h-screen flex flex-col antialiased">
|
||||||
|
<!-- TOP BAR (32px) -->
|
||||||
|
<header class="h-8 bg-surface-container border-b border-outline flex items-center justify-between px-4 z-50">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-primary-container font-bold">▌</span>
|
||||||
|
<h1 class="font-headline text-[14px] font-bold tracking-tight text-on-surface">RS_TERMINAL_OS</h1>
|
||||||
|
</div>
|
||||||
|
<nav class="flex gap-4 font-label-caps text-[12px] uppercase tracking-widest">
|
||||||
|
<span class="text-primary-container">[ HOME ]</span>
|
||||||
|
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· PLAY</span>
|
||||||
|
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· STATS</span>
|
||||||
|
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· SETTINGS</span>
|
||||||
|
</nav>
|
||||||
|
<div class="flex items-center gap-3 font-label-caps text-[11px] text-on-surface-variant">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span>LV 12</span>
|
||||||
|
<span class="text-outline">|</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>XP 320/500</span>
|
||||||
|
<div class="w-[60px] h-1 bg-surface-container-highest">
|
||||||
|
<div class="h-full bg-primary-container w-[64%]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-outline">|</span>
|
||||||
|
<div class="flex items-center gap-1 text-info">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-info"></span>
|
||||||
|
<span class="uppercase">Synced</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-outline">|</span>
|
||||||
|
<span class="text-outline">v0.20.0</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- MAIN CONTENT AREA -->
|
||||||
|
<main class="flex-1 flex overflow-hidden">
|
||||||
|
<!-- LEFT PANE (40%) -->
|
||||||
|
<section class="w-[40%] border-r border-outline flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-outline font-label-caps text-xs">▌play.tsx</p>
|
||||||
|
<h2 class="font-headline text-[32px] font-bold text-on-surface leading-none uppercase">Ready to play?</h2>
|
||||||
|
<p class="text-on-surface-variant font-label-caps text-sm tracking-wide">RESUME · 12:34 ELAPSED · DRAW-3</p>
|
||||||
|
</div>
|
||||||
|
<button class="w-full h-24 bg-primary-container text-surface font-headline text-[24px] font-bold flex items-center justify-center gap-4 hover:brightness-110 active:scale-[0.98] transition-all">
|
||||||
|
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
|
||||||
|
CONTINUE GAME
|
||||||
|
</button>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<button class="h-12 border border-outline bg-transparent text-on-surface font-label-caps text-sm hover:border-primary-container hover:text-primary-container transition-all flex items-center justify-center gap-2">
|
||||||
|
<span class="material-symbols-outlined">add</span>
|
||||||
|
NEW GAME
|
||||||
|
</button>
|
||||||
|
<button class="h-12 border border-outline bg-transparent text-on-surface font-label-caps text-sm hover:border-primary-container hover:text-primary-container transition-all flex items-center justify-center gap-2">
|
||||||
|
<span class="material-symbols-outlined">refresh</span>
|
||||||
|
RESTART RUN
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Game Modes</p>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<!-- Zen -->
|
||||||
|
<div class="aspect-square border border-outline flex flex-col items-center justify-center gap-2 hover:bg-surface-container transition-colors cursor-pointer group">
|
||||||
|
<span class="material-symbols-outlined text-outline group-hover:text-primary-container">spa</span>
|
||||||
|
<span class="font-label-caps text-[10px] uppercase">Zen</span>
|
||||||
|
</div>
|
||||||
|
<!-- Time Attack -->
|
||||||
|
<div class="aspect-square border border-outline flex flex-col items-center justify-center gap-2 hover:bg-surface-container transition-colors cursor-pointer group">
|
||||||
|
<span class="material-symbols-outlined text-outline group-hover:text-primary-container">timer</span>
|
||||||
|
<span class="font-label-caps text-[10px] uppercase text-center">Time<br/>Attack</span>
|
||||||
|
</div>
|
||||||
|
<!-- Locked Challenge -->
|
||||||
|
<div class="aspect-square bg-[#0d0d0d] border border-outline/30 flex flex-col items-center justify-center gap-2 relative opacity-60">
|
||||||
|
<span class="material-symbols-outlined text-outline">lock</span>
|
||||||
|
<span class="font-label-caps text-[10px] uppercase">Challenge</span>
|
||||||
|
<div class="absolute -top-2 -right-2 bg-warning text-surface px-1 py-0.5 text-[8px] font-bold">LV 5</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- VISUAL DECORATION (IMAGE PLACEHOLDER) -->
|
||||||
|
<div class="mt-auto pt-8">
|
||||||
|
<div class="w-full h-40 border border-outline overflow-hidden">
|
||||||
|
<img class="w-full h-full object-cover opacity-40 grayscale hover:grayscale-0 transition-all duration-700" data-alt="A dark, high-contrast digital art piece showing an abstract terminal interface with glowing cyan scanlines and retro-futuristic grid patterns. The composition is geometric and minimalist, following a synthwave aesthetic with deep black backgrounds and crisp cyan light elements. The lighting is moody and artificial, suggesting a high-performance computer screen in a dimly lit server room. Professional, sharp-edged UI design style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAet8SrRWSacZfwd8ISRQdDC7CDGixBwRnPAVMmMcjbifq1jnHSzCGWgSSL6YPSRfCkLNWr91BxTzV4zigGjMBLlk7rCLo5I7X7F6ydinDrKJVqZkRbvHJeSo90BPANoQwZtzPvhKXVEA9C2DbBaj8KPR4ObCo24Mj25NXPvGNThOE-3BSpuU6MPC-hrUMPVCPJpZnJdI_OmSz8mT021vjTxFERN12S1PFOzXKmNUDleoTDIat-8UifyKmKg4eKilecrBW6sFqaBw"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- CENTER PANE (30%) -->
|
||||||
|
<section class="w-[30%] border-r border-outline flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-outline font-label-caps text-xs">▌daily.json</p>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="font-headline text-[18px] font-bold text-on-surface">MAY 07 · 2026</h3>
|
||||||
|
<span class="bg-warning/20 text-warning px-2 py-1 text-[10px] font-bold border border-warning/40">EXPIRES 11:42:30</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface-container p-6 border border-outline space-y-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-on-surface-variant font-label-caps text-[10px] uppercase tracking-tighter">Current Seed</p>
|
||||||
|
<p class="font-headline text-[24px] font-extrabold text-highlight-valid">#2024-127</p>
|
||||||
|
</div>
|
||||||
|
<button class="w-full py-3 bg-primary-container text-surface font-label-caps text-xs font-bold uppercase tracking-widest hover:brightness-110 active:scale-95 transition-all">
|
||||||
|
▶ Attempt Today
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Global Standings</p>
|
||||||
|
<div class="space-y-1 text-xs font-label-caps">
|
||||||
|
<div class="flex justify-between py-2 border-b border-outline/30 text-highlight-valid">
|
||||||
|
<span>01 │ swift_jaguar</span>
|
||||||
|
<span>02:47</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
|
||||||
|
<span>02 │ pixel_drifter</span>
|
||||||
|
<span>03:12</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
|
||||||
|
<span>03 │ null_ptr</span>
|
||||||
|
<span>03:15</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
|
||||||
|
<span>04 │ core_dump_88</span>
|
||||||
|
<span>03:44</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between py-2 text-primary-container bg-primary-container/10 px-2 -mx-2">
|
||||||
|
<span>12 │ YOU (anon)</span>
|
||||||
|
<span>--:--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- RIGHT PANE (30%) -->
|
||||||
|
<section class="w-[30%] flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-outline font-label-caps text-xs">▌stats.log</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="border border-outline p-4 space-y-1">
|
||||||
|
<p class="text-on-surface-variant text-[10px] uppercase">Games</p>
|
||||||
|
<p class="font-hud-score text-[28px] text-on-surface">247</p>
|
||||||
|
</div>
|
||||||
|
<div class="border border-outline p-4 space-y-1 text-highlight-valid">
|
||||||
|
<p class="text-on-surface-variant text-[10px] uppercase">Win Rate</p>
|
||||||
|
<p class="font-hud-score text-[28px]">61%</p>
|
||||||
|
</div>
|
||||||
|
<div class="border border-outline p-4 space-y-1">
|
||||||
|
<p class="text-on-surface-variant text-[10px] uppercase">Best Time</p>
|
||||||
|
<p class="font-hud-score text-[28px]">01:54</p>
|
||||||
|
</div>
|
||||||
|
<div class="border border-outline p-4 space-y-1 text-primary-container">
|
||||||
|
<p class="text-on-surface-variant text-[10px] uppercase">Streak</p>
|
||||||
|
<p class="font-hud-score text-[28px]">7</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Achievements (8/19)</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<!-- Filled Cyan Dots -->
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<div class="w-3 h-3 bg-primary-container"></div>
|
||||||
|
<!-- Empty Dots -->
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
<div class="w-3 h-3 border border-outline"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto border border-outline bg-surface-container p-4 flex items-center justify-between hover:border-primary-container transition-colors cursor-pointer group">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-primary-container text-surface flex items-center justify-center font-bold text-lg">RS</div>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<p class="text-on-surface font-bold text-xs">anonymous@local</p>
|
||||||
|
<p class="text-on-surface-variant text-[10px]">Session: Active</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="material-symbols-outlined text-primary-container group-hover:translate-x-1 transition-transform">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- BOTTOM BAR (24px) -->
|
||||||
|
<footer class="h-6 bg-surface-container border-t border-outline flex items-center justify-between px-4 text-[10px] font-label-caps">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-primary-container">▌ NORMAL</span>
|
||||||
|
<span class="text-outline">│</span>
|
||||||
|
<span class="text-on-surface-variant">~/rusty-solitaire/home</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 text-on-surface-variant">
|
||||||
|
<div class="flex items-center gap-1"><span class="text-primary-container">[SPACE]</span> play</div>
|
||||||
|
<div class="flex items-center gap-1"><span class="text-primary-container">[D]</span> daily</div>
|
||||||
|
<div class="flex items-center gap-1"><span class="text-primary-container">[S]</span> settings</div>
|
||||||
|
<div class="flex items-center gap-1"><span class="text-primary-container">[?]</span> help</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-outline">
|
||||||
|
2026-05-07 17:42 EDT
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- GLOBAL SCANLINE EFFECT -->
|
||||||
|
<div class="fixed inset-0 pointer-events-none z-[100] overflow-hidden opacity-10">
|
||||||
|
<div class="absolute inset-0" style="background: repeating-linear-gradient(0deg, #151515, #151515 2px, #202020 4px);"></div>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||