Compare commits
91 Commits
77df2d2aef
...
v0.25.1
| Author | SHA1 | Date | |
|---|---|---|---|
| ab35fcf906 | |||
| 32991301dd | |||
| c5fd928dcb | |||
| f6907671be | |||
| a54fff7257 | |||
| 533bcec2d8 | |||
| ba786f5a09 | |||
| 7ee7cb6d93 | |||
| 14324b09ef | |||
| 124f1f5cf5 | |||
| a6a73b5f36 | |||
| b84fe79806 | |||
| 3248f00d66 | |||
| c680a043ae | |||
| d0ab7ed97b | |||
| 1144a96757 | |||
| ac6668cee7 | |||
| eba1f66b45 | |||
| 90959728b1 | |||
| 8b30f8778b | |||
| d6a7924f14 | |||
| 4db43fb3fb | |||
| 01d6b27e61 | |||
| 3cffbc2c51 | |||
| 2ef25934ac | |||
| bb670d6cc6 | |||
| 76911c57c9 | |||
| 8391235a1a | |||
| 2f3a6b9586 | |||
| 4d20b70809 | |||
| bfadcf0e0d | |||
| 356dbebe57 | |||
| c90c783177 | |||
| bbf4b2c14a | |||
| 62be72e918 | |||
| 1f46785b31 | |||
| 2e5d82f83c | |||
| 396ba6bc97 | |||
| 88298206bb | |||
| 0f65031114 | |||
| c91ce9436e | |||
| ace96b4a47 | |||
| ea079af9e1 | |||
| c66d81c73a | |||
| 20b7a617e0 | |||
| 7a0d57b2b1 | |||
| 93ec4a7478 | |||
| 72dfd741c4 | |||
| 3837a10b15 | |||
| 574115cb71 | |||
| 1707553790 | |||
| 6905f26b56 | |||
| 1b7c4d92aa | |||
| d685224ce6 | |||
| 539779d78b | |||
| f6506c57e5 | |||
| b88f3df119 | |||
| 0dcb783e94 | |||
| ea17f94b6c | |||
| d60dc18add | |||
| 38eefb22e8 | |||
| a579c25d5c | |||
| c40817d845 | |||
| c6c03b8bff | |||
| 5b3925a619 | |||
| 8485b3d1e0 | |||
| 8325bf6cf7 | |||
| ea58f5dd64 | |||
| c518255a2d | |||
| f5da9398f2 | |||
| b82573e7b1 | |||
| 40818f5bd2 | |||
| 228ebbad8a | |||
| 2b33feafc9 | |||
| f8c8c9158e | |||
| 9cc0837088 | |||
| b47462bd27 | |||
| 08d22c822a | |||
| feb581005c | |||
| 00f2d890f1 | |||
| 9533a7d420 | |||
| 5ec5ac1a19 | |||
| 86aea206b8 | |||
| 1bd1c0f927 | |||
| 7be7f4395c | |||
| 66c2907c25 | |||
| c2811fa661 | |||
| 933cc55ea9 | |||
| 58faae1911 | |||
| 96be1b85fb | |||
| bbf7709912 |
@@ -0,0 +1,131 @@
|
||||
name: Android Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
# Rebuild whenever app/engine/asset code changes.
|
||||
# Skip server-only, deploy, and doc changes.
|
||||
paths-ignore:
|
||||
- 'deploy/**'
|
||||
- 'argocd/**'
|
||||
- 'solitaire_server/**'
|
||||
- '**.md'
|
||||
|
||||
env:
|
||||
ANDROID_SDK: /opt/android-sdk
|
||||
NDK_VERSION: "25.2.9519653"
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
PLATFORM: "android-34"
|
||||
|
||||
jobs:
|
||||
build-apk:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set short SHA
|
||||
id: meta
|
||||
run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── System dependencies ────────────────────────────────────────────
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y openjdk-17-jdk-headless unzip zip
|
||||
|
||||
# ── Android SDK (shared cache key with release workflow) ──────────
|
||||
- name: Cache Android SDK
|
||||
uses: actions/cache@v4
|
||||
id: sdk-cache
|
||||
with:
|
||||
path: ${{ env.ANDROID_SDK }}
|
||||
key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
||||
|
||||
- name: Install Android SDK + NDK
|
||||
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
sudo mkdir -p ${{ env.ANDROID_SDK }}/cmdline-tools
|
||||
curl -sL \
|
||||
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||
-o /tmp/cmdtools.zip
|
||||
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||
sudo mv /tmp/cmdtools/cmdline-tools ${{ env.ANDROID_SDK }}/cmdline-tools/latest
|
||||
yes | sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||
--sdk_root=${{ env.ANDROID_SDK }} --licenses > /dev/null 2>&1 || true
|
||||
sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||
--sdk_root=${{ env.ANDROID_SDK }} \
|
||||
"build-tools;${{ env.BUILD_TOOLS_VERSION }}" \
|
||||
"platforms;${{ env.PLATFORM }}" \
|
||||
"ndk;${{ env.NDK_VERSION }}"
|
||||
|
||||
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||
- name: Install Rust stable
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --default-toolchain stable --no-modify-path
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Add Android cross-compilation targets
|
||||
run: |
|
||||
rustup target add \
|
||||
aarch64-linux-android \
|
||||
armv7-linux-androideabi \
|
||||
x86_64-linux-android
|
||||
|
||||
# ── Cargo caches ───────────────────────────────────────────────────
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: cargo-registry-
|
||||
|
||||
- name: Cache cargo-ndk binary
|
||||
uses: actions/cache@v4
|
||||
id: ndk-tool-cache
|
||||
with:
|
||||
path: ~/.cargo/bin/cargo-ndk
|
||||
key: cargo-ndk-${{ runner.os }}-stable
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: android-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
||||
restore-keys: android-target-${{ hashFiles('**/Cargo.lock') }}-
|
||||
|
||||
- name: Install cargo-ndk
|
||||
if: steps.ndk-tool-cache.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-ndk --locked
|
||||
|
||||
# ── Build APK ──────────────────────────────────────────────────────
|
||||
# Debug CI only builds arm64-v8a — full three-ABI debug builds blow
|
||||
# past the runner's disk budget (~25 GB of target/ + intermediate
|
||||
# APKs caused apksigner to OOM-on-disk in the previous run). Release
|
||||
# CI still ships all three ABIs from android-release.yml.
|
||||
- name: Build debug APK
|
||||
env:
|
||||
ANDROID_HOME: ${{ env.ANDROID_SDK }}
|
||||
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }}
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOLS_VERSION }}
|
||||
PLATFORM: ${{ env.PLATFORM }}
|
||||
PROFILE: debug
|
||||
ABIS: arm64-v8a
|
||||
run: ./scripts/build_android_apk.sh
|
||||
|
||||
# ── Artifact ───────────────────────────────────────────────────────
|
||||
# Pinned to v3 because Gitea Actions doesn't implement the github.com
|
||||
# artifact service that upload-artifact@v4+ requires; v3 uses the
|
||||
# older chunked HTTP API that Gitea's GHES-compatibility layer
|
||||
# supports.
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: solitaire-quest-debug-${{ steps.meta.outputs.sha }}
|
||||
path: target/debug/apk/solitaire-quest.apk
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,165 @@
|
||||
name: Android Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
env:
|
||||
ANDROID_SDK: /opt/android-sdk
|
||||
NDK_VERSION: "25.2.9519653"
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
PLATFORM: "android-34"
|
||||
GITEA_API: https://git.aleshym.co/api/v1
|
||||
REPO: funman300/Rusty_Solitare
|
||||
|
||||
jobs:
|
||||
build-release-apk:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version from tag
|
||||
id: meta
|
||||
run: echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Free disk space ────────────────────────────────────────────────
|
||||
# A 2-ABI release build (arm64 + armv7) generates ~15 GB of target/
|
||||
# output. Remove pre-installed runner tooling that is never used
|
||||
# during an Android build to reclaim ~10 GB before we start.
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/local/lib/android # runner pre-installed SDK
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
df -h /
|
||||
|
||||
# ── System dependencies ────────────────────────────────────────────
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y openjdk-17-jdk-headless unzip zip jq
|
||||
|
||||
# ── Android SDK (shared cache key with debug workflow) ─────────────
|
||||
- name: Cache Android SDK
|
||||
uses: actions/cache@v4
|
||||
id: sdk-cache
|
||||
with:
|
||||
path: ${{ env.ANDROID_SDK }}
|
||||
key: v2-android-sdk-ndk${{ env.NDK_VERSION }}-bt${{ env.BUILD_TOOLS_VERSION }}
|
||||
|
||||
- name: Install Android SDK + NDK
|
||||
if: steps.sdk-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
sudo mkdir -p ${{ env.ANDROID_SDK }}/cmdline-tools
|
||||
curl -sL \
|
||||
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \
|
||||
-o /tmp/cmdtools.zip
|
||||
unzip -q /tmp/cmdtools.zip -d /tmp/cmdtools
|
||||
sudo mv /tmp/cmdtools/cmdline-tools ${{ env.ANDROID_SDK }}/cmdline-tools/latest
|
||||
yes | sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||
--sdk_root=${{ env.ANDROID_SDK }} --licenses > /dev/null 2>&1 || true
|
||||
sudo ${{ env.ANDROID_SDK }}/cmdline-tools/latest/bin/sdkmanager \
|
||||
--sdk_root=${{ env.ANDROID_SDK }} \
|
||||
"build-tools;${{ env.BUILD_TOOLS_VERSION }}" \
|
||||
"platforms;${{ env.PLATFORM }}" \
|
||||
"ndk;${{ env.NDK_VERSION }}"
|
||||
|
||||
# ── Rust toolchain ─────────────────────────────────────────────────
|
||||
- name: Install Rust stable
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --default-toolchain stable --no-modify-path
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Add Android cross-compilation targets
|
||||
run: |
|
||||
rustup target add \
|
||||
aarch64-linux-android \
|
||||
armv7-linux-androideabi
|
||||
|
||||
# ── Cargo caches ───────────────────────────────────────────────────
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: cargo-registry-
|
||||
|
||||
- name: Cache cargo-ndk binary
|
||||
uses: actions/cache@v4
|
||||
id: ndk-tool-cache
|
||||
with:
|
||||
path: ~/.cargo/bin/cargo-ndk
|
||||
key: cargo-ndk-${{ runner.os }}-stable
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: android-release-target-${{ hashFiles('**/Cargo.lock') }}-${{ github.sha }}
|
||||
restore-keys: android-release-target-${{ hashFiles('**/Cargo.lock') }}-
|
||||
|
||||
- name: Install cargo-ndk
|
||||
if: steps.ndk-tool-cache.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-ndk --locked
|
||||
|
||||
# ── Build & sign with release keystore ─────────────────────────────
|
||||
- name: Decode keystore
|
||||
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > /tmp/solitaire-release.jks
|
||||
|
||||
- name: Build signed release APK
|
||||
env:
|
||||
ANDROID_HOME: ${{ env.ANDROID_SDK }}
|
||||
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK }}/ndk/${{ env.NDK_VERSION }}
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOLS_VERSION }}
|
||||
PLATFORM: ${{ env.PLATFORM }}
|
||||
PROFILE: release
|
||||
# arm64-v8a covers all modern Android phones; armeabi-v7a covers
|
||||
# legacy ARM devices. x86_64 is emulator-only and dropped to
|
||||
# stay within the runner's ~25 GB disk budget.
|
||||
ABIS: arm64-v8a armeabi-v7a
|
||||
KEYSTORE: /tmp/solitaire-release.jks
|
||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASS: ${{ secrets.KEY_PASS }}
|
||||
APK_OUT: ferrous-solitaire-${{ steps.meta.outputs.tag }}.apk
|
||||
run: ./scripts/build_android_apk.sh
|
||||
|
||||
# ── Publish to Gitea release ───────────────────────────────────────
|
||||
- name: Create Gitea release
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
RESPONSE=$(curl -s -o /tmp/release.json -w "%{http_code}" \
|
||||
-X POST "$GITEA_API/repos/$REPO/releases" \
|
||||
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}")
|
||||
if [ "$RESPONSE" = "409" ]; then
|
||||
curl -sf "$GITEA_API/repos/$REPO/releases/tags/$TAG" \
|
||||
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||||
> /tmp/release.json
|
||||
elif [ "$RESPONSE" != "201" ]; then
|
||||
echo "Release creation failed with HTTP $RESPONSE"
|
||||
cat /tmp/release.json
|
||||
exit 1
|
||||
fi
|
||||
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
|
||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload signed APK
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
APK="ferrous-solitaire-${TAG}.apk"
|
||||
curl -sf -X POST \
|
||||
"$GITEA_API/repos/$REPO/releases/${{ steps.release.outputs.release_id }}/assets?name=$APK" \
|
||||
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$APK"
|
||||
@@ -36,6 +36,11 @@ jobs:
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.CI_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -45,6 +50,8 @@ jobs:
|
||||
tags: |
|
||||
${{ env.IMAGE }}:${{ steps.meta.outputs.sha }}
|
||||
${{ env.IMAGE }}:latest
|
||||
cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Install kustomize
|
||||
run: |
|
||||
@@ -62,4 +69,5 @@ jobs:
|
||||
git config user.name "Gitea CI"
|
||||
git add deploy/kustomization.yaml
|
||||
git commit -m "chore(deploy): bump image to ${{ steps.meta.outputs.sha }} [skip ci]" || true
|
||||
git pull --rebase origin master
|
||||
git push
|
||||
|
||||
@@ -14,4 +14,10 @@ data/
|
||||
# Android signing keystores — never commit
|
||||
*.jks
|
||||
*.jks.bak
|
||||
*.jks.bak*
|
||||
*.keystore
|
||||
|
||||
# Kubernetes secrets — apply manually, never commit
|
||||
deploy/matomo-secret.yaml
|
||||
deploy/*-secret.yaml
|
||||
deploy/*-auth-secret.yaml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE leaderboard\n SET best_score = ?,\n best_time_secs = ?,\n recorded_at = ?\n WHERE user_id = ?\n AND (\n best_score IS NULL\n OR ? > best_score\n OR (? = best_score AND (best_time_secs IS NULL OR ? < best_time_secs))\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0e199cafab7e71b0c7f28ede85a622e38649d2fe5a73a5c715f2319f5450f729"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT l.display_name, l.best_score, l.best_time_secs, l.recorded_at\n FROM leaderboard l\n JOIN users u ON u.id = l.user_id\n WHERE u.leaderboard_opt_in = 1\n ORDER BY\n CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,\n l.best_score DESC,\n CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,\n l.best_time_secs ASC",
|
||||
"query": "SELECT l.display_name, l.best_score, l.best_time_secs, l.recorded_at\n FROM leaderboard l\n JOIN users u ON u.id = l.user_id\n WHERE u.leaderboard_opt_in = 1\n ORDER BY\n CASE WHEN l.best_score IS NULL THEN 1 ELSE 0 END ASC,\n l.best_score DESC,\n CASE WHEN l.best_time_secs IS NULL THEN 1 ELSE 0 END ASC,\n l.best_time_secs ASC\n LIMIT 100",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -34,5 +34,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "57c93a6acd7eea44d00412e62f0d3fed7ffbe4cd759353d29f38a8eb37f69112"
|
||||
"hash": "2b814989a6632ca930ae1e895f97a7fc3389c91d1d2abf6900a21fb0d6e94ef3"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR IGNORE INTO analytics_events\n (id, user_id, session_id, event_type, payload, client_time, received_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f23630e78ae88e72d7930184f7cd8fc67e71f3930609f1cf14061d35d6de8ec3"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Solitaire Quest — Architecture Document
|
||||
# Ferrous Solitaire — Architecture Document
|
||||
|
||||
> **Version:** 1.3
|
||||
> **Language:** Rust (Edition 2024)
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops. It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
|
||||
Ferrous Solitaire is a cross-platform Klondike Solitaire game written in Rust, targeting macOS, Windows, and Linux desktops. It features a full progression system with XP, levels, achievements, daily challenges, and an optional self-hosted sync server so statistics and progress are available across all of a player's devices.
|
||||
|
||||
### Sync Backend by Platform
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Solitaire Quest are documented here. The format is
|
||||
All notable changes to Ferrous Solitaire are documented here. The format is
|
||||
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
|
||||
project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Credits
|
||||
|
||||
Solitaire Quest is MIT-licensed (see [LICENSE](LICENSE)). It is built on top of
|
||||
Ferrous Solitaire is MIT-licensed (see [LICENSE](LICENSE)). It is built on top of
|
||||
the work of many open-source projects and a small handful of third-party
|
||||
assets. This file lists every component that ships in the binary or in the
|
||||
`assets/` directory.
|
||||
@@ -43,7 +43,7 @@ copyleft code is statically linked into the game binary.
|
||||
| File(s) | Source | License |
|
||||
|---|---|---|
|
||||
| `solitaire_engine/assets/themes/default/{suit}_{rank}.svg` (52 SVGs) | [hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets) | MIT |
|
||||
| `solitaire_engine/assets/themes/default/back.svg` | Original — Solitaire Quest | MIT (this project) |
|
||||
| `solitaire_engine/assets/themes/default/back.svg` | Original — Ferrous Solitaire | MIT (this project) |
|
||||
| `assets/cards/faces/{RANK}{SUIT}.png` (52 PNGs) | Pre-rendered from the same `playing-cards-assets` SVGs | MIT (passed through from hayeah) |
|
||||
| `assets/cards/backs/back_0.png` – `back_4.png` | Original — generated by `solitaire_assetgen::gen_art` | MIT (this project) |
|
||||
|
||||
@@ -107,6 +107,6 @@ Audio files are MIT-licensed alongside the rest of this project.
|
||||
backs, every audio file) are original work covered by this project's MIT
|
||||
license.
|
||||
|
||||
If you redistribute Solitaire Quest, you must ship this `CREDITS.md` and the
|
||||
If you redistribute Ferrous Solitaire, you must ship this `CREDITS.md` and the
|
||||
`LICENSE` file alongside the binary so the MIT (project + hayeah card art)
|
||||
and OFL (FiraMono) notices remain visible to end users.
|
||||
|
||||
@@ -7018,6 +7018,7 @@ dependencies = [
|
||||
"resvg",
|
||||
"ron",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solitaire_core",
|
||||
"solitaire_data",
|
||||
"solitaire_sync",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Solitaire Quest
|
||||
# Ferrous Solitaire
|
||||
|
||||
A cross-platform Klondike Solitaire game written in Rust, with a card-theme
|
||||
system, full progression (XP / levels / achievements / daily challenges), and
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Solitaire Quest — Self-Hosting Guide
|
||||
# Ferrous Solitaire — Self-Hosting Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Solitaire Quest — Session Handoff
|
||||
# Ferrous Solitaire — Session Handoff
|
||||
|
||||
**Last updated:** 2026-05-12 — Leaderboard display name shipped (`03be4fc`). All commits pushed to origin.
|
||||
|
||||
@@ -150,7 +150,7 @@ Items missing from the doc:
|
||||
## Resume prompt
|
||||
|
||||
```
|
||||
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||
You are a senior Rust + Bevy developer working on Ferrous Solitaire.
|
||||
Working directory: <Rusty_Solitaire clone path>.
|
||||
Branch: master. v0.23.0 is the current version (HEAD: 03be4fc). Fully pushed.
|
||||
|
||||
|
||||
@@ -6,12 +6,20 @@ metadata:
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: http://10.10.0.64:3000/funman300/Rusty_Solitare.git
|
||||
repoURL: https://git.aleshym.co/funman300/Rusty_Solitare.git
|
||||
targetRevision: master
|
||||
path: deploy
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: solitaire
|
||||
# Secrets are applied manually and must not be pruned by ArgoCD.
|
||||
ignoreDifferences:
|
||||
- group: ""
|
||||
kind: Secret
|
||||
name: matomo-secret
|
||||
namespace: solitaire
|
||||
jsonPointers:
|
||||
- /data
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: solitaire-analytics
|
||||
namespace: solitaire
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: analytics.aleshym.co
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: matomo
|
||||
port:
|
||||
name: http
|
||||
tls:
|
||||
- hosts:
|
||||
- analytics.aleshym.co
|
||||
secretName: analytics-tls
|
||||
@@ -7,10 +7,17 @@ resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
- mariadb-pvc.yaml
|
||||
- mariadb-deployment.yaml
|
||||
- mariadb-service.yaml
|
||||
- matomo-pvc.yaml
|
||||
- matomo-deployment.yaml
|
||||
- matomo-service.yaml
|
||||
- ingress-analytics.yaml
|
||||
|
||||
# CI updates this block automatically via `kustomize edit set image`.
|
||||
# The image name here matches the `image: solitaire-server` stub in deployment.yaml.
|
||||
images:
|
||||
- name: solitaire-server
|
||||
newName: git.aleshym.co/funman300/solitaire-server
|
||||
newTag: latest
|
||||
newTag: f6907671
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mariadb
|
||||
namespace: solitaire
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mariadb
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mariadb
|
||||
spec:
|
||||
containers:
|
||||
- name: mariadb
|
||||
image: mariadb:11
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matomo-secret
|
||||
key: MYSQL_ROOT_PASSWORD
|
||||
- name: MYSQL_DATABASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matomo-secret
|
||||
key: MYSQL_DATABASE
|
||||
- name: MYSQL_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matomo-secret
|
||||
key: MYSQL_USER
|
||||
- name: MYSQL_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matomo-secret
|
||||
key: MYSQL_PASSWORD
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
volumeMounts:
|
||||
- name: mariadb-data
|
||||
mountPath: /var/lib/mysql
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- healthcheck.sh
|
||||
- --connect
|
||||
- --innodb_initialized
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- healthcheck.sh
|
||||
- --connect
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
volumes:
|
||||
- name: mariadb-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mariadb-data
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mariadb-data
|
||||
namespace: solitaire
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mariadb
|
||||
namespace: solitaire
|
||||
spec:
|
||||
selector:
|
||||
app: mariadb
|
||||
ports:
|
||||
- name: mysql
|
||||
port: 3306
|
||||
targetPort: 3306
|
||||
clusterIP: None
|
||||
@@ -0,0 +1,79 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: matomo
|
||||
namespace: solitaire
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: matomo
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: matomo
|
||||
spec:
|
||||
containers:
|
||||
- name: matomo
|
||||
image: matomo:5.10.0
|
||||
env:
|
||||
- name: MATOMO_DATABASE_HOST
|
||||
value: mariadb
|
||||
- name: MATOMO_DATABASE_PORT
|
||||
value: "3306"
|
||||
- name: MATOMO_DATABASE_ADAPTER
|
||||
value: PDO\MYSQL
|
||||
- name: MATOMO_DATABASE_DBNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matomo-secret
|
||||
key: MYSQL_DATABASE
|
||||
- name: MATOMO_DATABASE_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matomo-secret
|
||||
key: MYSQL_USER
|
||||
- name: MATOMO_DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matomo-secret
|
||||
key: MYSQL_PASSWORD
|
||||
# Traefik terminates SSL; tell Matomo to trust X-Forwarded-* headers
|
||||
- name: MATOMO_GENERAL_ASSUME_SECURE_PROTOCOL
|
||||
value: "1"
|
||||
- name: MATOMO_GENERAL_PROXY_CLIENT_HEADERS
|
||||
value: HTTP_X_FORWARDED_FOR
|
||||
- name: MATOMO_GENERAL_PROXY_HOST_HEADERS
|
||||
value: HTTP_X_FORWARDED_HOST
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
- name: matomo-data
|
||||
mountPath: /var/www/html
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /matomo.php
|
||||
port: 80
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /matomo.php
|
||||
port: 80
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
volumes:
|
||||
- name: matomo-data
|
||||
persistentVolumeClaim:
|
||||
claimName: matomo-data
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: matomo-data
|
||||
namespace: solitaire
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
@@ -0,0 +1,22 @@
|
||||
# DO NOT COMMIT THE REAL VERSION OF THIS FILE.
|
||||
# deploy/matomo-secret.yaml is gitignored — apply it manually once:
|
||||
#
|
||||
# cp deploy/matomo-secret.yaml.example deploy/matomo-secret.yaml
|
||||
# # edit the passwords below, then:
|
||||
# kubectl apply -f deploy/matomo-secret.yaml
|
||||
# kubectl annotate secret matomo-secret -n solitaire \
|
||||
# argocd.argoproj.io/sync-options=Prune=false --overwrite
|
||||
#
|
||||
# Generate strong passwords with:
|
||||
# python3 -c "import secrets; print(secrets.token_urlsafe(18))"
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: matomo-secret
|
||||
namespace: solitaire
|
||||
stringData:
|
||||
MYSQL_ROOT_PASSWORD: "CHANGE_ME"
|
||||
MYSQL_DATABASE: matomo
|
||||
MYSQL_USER: matomo
|
||||
MYSQL_PASSWORD: "CHANGE_ME"
|
||||
MATOMO_ADMIN_PASSWORD: "CHANGE_ME"
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: matomo
|
||||
namespace: solitaire
|
||||
spec:
|
||||
selector:
|
||||
app: matomo
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
@@ -1,4 +1,4 @@
|
||||
# Solitaire Quest — Session Handoff (ARCHIVED)
|
||||
# Ferrous Solitaire — Session Handoff (ARCHIVED)
|
||||
|
||||
> **This file is from Phase 2 (2026-04-25, 242 tests). It is kept for historical
|
||||
> reference only. The authoritative session handoff is at the repo root:
|
||||
@@ -24,7 +24,7 @@ All seven Cargo crates created and compiling cleanly:
|
||||
| `solitaire_engine` | Stub | Bevy ECS systems — all plugins added in Phase 3 |
|
||||
| `solitaire_server` | Stub | Axum sync server — implemented in Phase 8C |
|
||||
| `solitaire_gpgs` | Compile-time stub | Google Play Games bridge — Android only, JNI in Phase: Android |
|
||||
| `solitaire_app` | Working | Opens blank Bevy window titled "Solitaire Quest" at 1280×800 |
|
||||
| `solitaire_app` | Working | Opens blank Bevy window titled "Ferrous Solitaire" at 1280×800 |
|
||||
|
||||
Fast compile profiles, `assets/` directory structure, and `.env.example` are all in place.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **Date:** 2026-04-28
|
||||
> **Author:** Claude Code
|
||||
> **Scope:** Feasibility analysis for porting Solitaire Quest to Android using cargo-mobile2
|
||||
> **Scope:** Feasibility analysis for porting Ferrous Solitaire to Android using cargo-mobile2
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Solitaire Quest — Phase 1 + 2: Workspace & Core Game Engine
|
||||
# Ferrous Solitaire — Phase 1 + 2: Workspace & Core Game Engine
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
@@ -555,7 +555,7 @@ fn main() {
|
||||
.add_plugins(
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Solitaire Quest".into(),
|
||||
title: "Ferrous Solitaire".into(),
|
||||
resolution: (1280.0, 800.0).into(),
|
||||
..default()
|
||||
}),
|
||||
@@ -571,7 +571,7 @@ fn main() {
|
||||
```bash
|
||||
cargo run -p solitaire_app --features bevy/dynamic_linking
|
||||
```
|
||||
Expected: A blank Bevy window titled "Solitaire Quest" opens. Press Escape or close the window to exit. No panics or errors in the terminal.
|
||||
Expected: A blank Bevy window titled "Ferrous Solitaire" opens. Press Escape or close the window to exit. No panics or errors in the terminal.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1210,7 +1210,7 @@ fn main() {
|
||||
.add_plugins(
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Solitaire Quest".into(),
|
||||
title: "Ferrous Solitaire".into(),
|
||||
resolution: (1280.0, 800.0).into(),
|
||||
..default()
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
### Infrastructure
|
||||
|
||||
- Two machines (or VMs) referred to as **Machine A** and **Machine B** throughout this runbook. Both must be able to reach the sync server over the network.
|
||||
- A running Solitaire Quest sync server reachable at a known URL, e.g. `https://solitaire.example.com`. See `README_SERVER.md` for setup.
|
||||
- A running Ferrous Solitaire sync server reachable at a known URL, e.g. `https://solitaire.example.com`. See `README_SERVER.md` for setup.
|
||||
- Verify the server is live before starting:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> **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
|
||||
> is named "Ferrous Solitaire *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
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a self-signed Android APK from solitaire_app's cdylib targets.
|
||||
#
|
||||
# Replaces the cargo-apk pipeline with explicit cargo-ndk + aapt2 + apksigner
|
||||
# steps. The CI runner was hitting an SDK-discovery bug inside cargo-apk's
|
||||
# ndk-build crate that we couldn't isolate; running each Android toolchain
|
||||
# step explicitly gives us a debuggable pipeline.
|
||||
#
|
||||
# Required environment:
|
||||
# ANDROID_HOME Path to Android SDK root
|
||||
# ANDROID_NDK_HOME Path to the specific NDK version
|
||||
# BUILD_TOOLS_VERSION e.g. "34.0.0"
|
||||
# PLATFORM e.g. "android-34"
|
||||
#
|
||||
# Optional environment:
|
||||
# PROFILE "debug" (default) | "release"
|
||||
# ABIS Space-separated Android ABIs to build (default:
|
||||
# "arm64-v8a armeabi-v7a x86_64"). Reduce in CI to
|
||||
# fit the runner's disk budget — a full three-ABI
|
||||
# debug build can exceed 25 GB of target/ output.
|
||||
# APK_OUT Output APK path (default: target/$PROFILE/apk/solitaire-quest.apk)
|
||||
# KEYSTORE Path to keystore for signing (default: generates a debug keystore)
|
||||
# KEYSTORE_PASS Keystore password (default: "android" for the generated debug keystore)
|
||||
# KEY_ALIAS Key alias (default: "androiddebugkey")
|
||||
# KEY_PASS Key password (default: same as KEYSTORE_PASS)
|
||||
#
|
||||
# Outputs:
|
||||
# $APK_OUT Signed, zipaligned APK
|
||||
set -euo pipefail
|
||||
|
||||
: "${ANDROID_HOME:?ANDROID_HOME must be set}"
|
||||
: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}"
|
||||
: "${BUILD_TOOLS_VERSION:?BUILD_TOOLS_VERSION must be set}"
|
||||
: "${PLATFORM:?PLATFORM must be set (e.g. android-34)}"
|
||||
|
||||
PROFILE="${PROFILE:-debug}"
|
||||
ABIS="${ABIS:-arm64-v8a armeabi-v7a x86_64}"
|
||||
APK_OUT="${APK_OUT:-target/${PROFILE}/apk/solitaire-quest.apk}"
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
BT="$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION"
|
||||
PLATFORM_JAR="$ANDROID_HOME/platforms/$PLATFORM/android.jar"
|
||||
MANIFEST="solitaire_app/android/AndroidManifest.xml"
|
||||
RES_DIR="solitaire_app/res"
|
||||
ASSETS_DIR="assets"
|
||||
|
||||
# --- sanity ----------------------------------------------------------------
|
||||
for f in "$BT/aapt2" "$BT/zipalign" "$BT/apksigner" "$PLATFORM_JAR" "$MANIFEST"; do
|
||||
[ -e "$f" ] || { echo "missing: $f"; exit 1; }
|
||||
done
|
||||
|
||||
STAGING="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGING"' EXIT
|
||||
mkdir -p "$STAGING/lib" "$STAGING/compiled-res"
|
||||
|
||||
# --- 1. native libraries via cargo-ndk -------------------------------------
|
||||
# `-o $STAGING/lib` lays out files as $STAGING/lib/<abi>/libsolitaire_app.so
|
||||
# which is the directory structure the APK expects under lib/.
|
||||
CARGO_NDK_ARGS=( --platform 26 -o "$STAGING/lib" )
|
||||
for abi in $ABIS; do
|
||||
CARGO_NDK_ARGS+=( -t "$abi" )
|
||||
done
|
||||
CARGO_NDK_ARGS+=( build --package solitaire_app --lib )
|
||||
if [ "$PROFILE" = "release" ]; then
|
||||
CARGO_NDK_ARGS+=( --release )
|
||||
fi
|
||||
echo ">>> cargo ndk ${CARGO_NDK_ARGS[*]}"
|
||||
cargo ndk "${CARGO_NDK_ARGS[@]}"
|
||||
|
||||
# --- 2. compile + link resources and manifest ------------------------------
|
||||
if [ -d "$RES_DIR" ]; then
|
||||
echo ">>> aapt2 compile resources"
|
||||
"$BT/aapt2" compile --dir "$RES_DIR" -o "$STAGING/compiled-res"
|
||||
fi
|
||||
|
||||
LINK_ARGS=(
|
||||
link
|
||||
-o "$STAGING/app-unsigned.apk"
|
||||
-I "$PLATFORM_JAR"
|
||||
--manifest "$MANIFEST"
|
||||
)
|
||||
[ -d "$ASSETS_DIR" ] && LINK_ARGS+=( -A "$ASSETS_DIR" )
|
||||
# Add compiled resources if any
|
||||
shopt -s nullglob
|
||||
RES_FLATS=( "$STAGING/compiled-res"/*.flat )
|
||||
shopt -u nullglob
|
||||
if [ ${#RES_FLATS[@]} -gt 0 ]; then
|
||||
LINK_ARGS+=( "${RES_FLATS[@]}" )
|
||||
fi
|
||||
echo ">>> aapt2 link"
|
||||
"$BT/aapt2" "${LINK_ARGS[@]}"
|
||||
|
||||
# --- 3. add native libraries to the APK ------------------------------------
|
||||
echo ">>> bundle native libraries"
|
||||
( cd "$STAGING" && zip -r -q app-unsigned.apk lib/ )
|
||||
|
||||
# --- 4. zipalign -----------------------------------------------------------
|
||||
echo ">>> zipalign"
|
||||
"$BT/zipalign" -p -f 4 "$STAGING/app-unsigned.apk" "$STAGING/app-aligned.apk"
|
||||
# Free the unsigned intermediate now — apksigner reads $app-aligned.apk and
|
||||
# writes $APK_OUT, and the runner's disk is tight after a multi-ABI build.
|
||||
rm -f "$STAGING/app-unsigned.apk"
|
||||
|
||||
# --- 5. sign ---------------------------------------------------------------
|
||||
if [ -z "${KEYSTORE:-}" ]; then
|
||||
# Generate a deterministic debug keystore on the fly.
|
||||
KEYSTORE="$STAGING/debug.keystore"
|
||||
KEYSTORE_PASS="${KEYSTORE_PASS:-android}"
|
||||
KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}"
|
||||
KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}"
|
||||
echo ">>> generating debug keystore at $KEYSTORE"
|
||||
keytool -genkeypair -v \
|
||||
-keystore "$KEYSTORE" \
|
||||
-storepass "$KEYSTORE_PASS" \
|
||||
-alias "$KEY_ALIAS" \
|
||||
-keypass "$KEY_PASS" \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-dname "CN=Android Debug,O=Android,C=US" > /dev/null
|
||||
fi
|
||||
|
||||
KEYSTORE_PASS="${KEYSTORE_PASS:-android}"
|
||||
KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}"
|
||||
KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}"
|
||||
|
||||
mkdir -p "$(dirname "$APK_OUT")"
|
||||
echo ">>> apksigner sign -> $APK_OUT"
|
||||
"$BT/apksigner" sign \
|
||||
--ks "$KEYSTORE" \
|
||||
--ks-pass "pass:$KEYSTORE_PASS" \
|
||||
--ks-key-alias "$KEY_ALIAS" \
|
||||
--key-pass "pass:$KEY_PASS" \
|
||||
--out "$APK_OUT" \
|
||||
"$STAGING/app-aligned.apk"
|
||||
|
||||
echo ">>> verify"
|
||||
"$BT/apksigner" verify --verbose "$APK_OUT"
|
||||
|
||||
echo ">>> done: $APK_OUT"
|
||||
@@ -87,7 +87,7 @@ required = true
|
||||
name = "android.permission.INTERNET"
|
||||
|
||||
[package.metadata.android.application]
|
||||
label = "Solitaire Quest"
|
||||
label = "Ferrous Solitaire"
|
||||
# Launcher icon — references the density-bucketed mipmap resource above.
|
||||
icon = "@mipmap/ic_launcher"
|
||||
# `debuggable` defaults to false on release builds; cargo-apk flips it
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Mirrors what cargo-apk would generate from [package.metadata.android]
|
||||
in solitaire_app/Cargo.toml. Kept in-tree so the CI workflow can drive
|
||||
aapt2 directly without going through cargo-apk's brittle SDK discovery.
|
||||
|
||||
Keep in sync with:
|
||||
* Cargo.toml: package, min_sdk_version, target_sdk_version,
|
||||
uses_feature, uses_permission, application label/icon,
|
||||
activity orientation
|
||||
* [lib].name (currently "solitaire_app") — matches the
|
||||
`android.app.lib_name` meta-data value below, which is the
|
||||
shared object name without the `lib` prefix or `.so` suffix.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.solitairequest.app"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="26"
|
||||
android:targetSdkVersion="34" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="Ferrous Solitaire"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:hasCode="false">
|
||||
|
||||
<activity
|
||||
android:name="android.app.NativeActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|navigation|screenLayout">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="solitaire_app" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -25,7 +25,7 @@ use bevy::window::{Monitor, PrimaryMonitor, PrimaryWindow};
|
||||
use bevy::winit::WinitWindows;
|
||||
use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings};
|
||||
use solitaire_engine::{
|
||||
register_theme_asset_sources, AchievementPlugin, AnimationPlugin, AssetSourcesPlugin,
|
||||
register_theme_asset_sources, AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin,
|
||||
AudioPlugin, AutoCompletePlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
||||
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
|
||||
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
||||
@@ -106,7 +106,7 @@ pub fn run() {
|
||||
DefaultPlugins
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Solitaire Quest".into(),
|
||||
title: "Ferrous Solitaire".into(),
|
||||
// X11/Wayland WM_CLASS so taskbar managers group
|
||||
// multiple windows of this app correctly.
|
||||
name: Some("solitaire-quest".into()),
|
||||
@@ -194,6 +194,7 @@ pub fn run() {
|
||||
.add_plugins(OnboardingPlugin)
|
||||
.add_plugins(SyncPlugin::new(sync_provider))
|
||||
.add_plugins(SyncSetupPlugin)
|
||||
.add_plugins(AnalyticsPlugin)
|
||||
.add_plugins(LeaderboardPlugin)
|
||||
.add_plugins(WinSummaryPlugin)
|
||||
.add_plugins(UiModalPlugin)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Generates PNG assets for Solitaire Quest.
|
||||
//! Generates PNG assets for Ferrous Solitaire.
|
||||
//!
|
||||
//! Produces:
|
||||
//! - 52 card face PNGs (120×168) — one per card, with rank, suit symbol, and
|
||||
|
||||
@@ -426,12 +426,11 @@ impl GameState {
|
||||
/// Returns `true` when stock and waste are empty and all tableau cards are face-up.
|
||||
/// At that point the game can be completed without further player input.
|
||||
pub fn check_auto_complete(&self) -> bool {
|
||||
// Stock must be empty; waste may still have cards (they are resolved
|
||||
// by draw() calls inside next_auto_complete_move / auto_complete_step).
|
||||
if !self.piles[&PileType::Stock].cards.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if !self.piles[&PileType::Waste].cards.is_empty() {
|
||||
return false;
|
||||
}
|
||||
(0..7).all(|i| {
|
||||
self.piles[&PileType::Tableau(i)]
|
||||
.cards
|
||||
@@ -459,17 +458,36 @@ impl GameState {
|
||||
if !self.is_auto_completable || self.is_won {
|
||||
return None;
|
||||
}
|
||||
// Check waste top first — when stock is exhausted the waste may still
|
||||
// contain cards that can go directly to a foundation.
|
||||
let waste = PileType::Waste;
|
||||
if let Some((card, slot)) = self.piles[&waste].cards.last()
|
||||
.and_then(|c| self.foundation_slot_for(c).map(|s| (c, s)))
|
||||
{
|
||||
let _ = card; // borrow ends here
|
||||
return Some((waste, PileType::Foundation(slot)));
|
||||
}
|
||||
for i in 0..7 {
|
||||
let tableau = PileType::Tableau(i);
|
||||
if let Some(card) = self.piles[&tableau].cards.last() {
|
||||
// Prefer the slot that already claims this card's suit so
|
||||
// Aces don't sometimes land in slot 0 and then leave the
|
||||
// matching suit-claimed slot empty.
|
||||
if let Some(slot) = self.piles[&tableau].cards.last()
|
||||
.and_then(|c| self.foundation_slot_for(c))
|
||||
{
|
||||
return Some((tableau, PileType::Foundation(slot)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the foundation slot index that `card` can legally move to, or
|
||||
/// `None` if no such slot exists.
|
||||
///
|
||||
/// Prefers the slot already claiming this card's suit so Aces always land
|
||||
/// in a consistent column. Falls back to an empty slot only for Aces.
|
||||
fn foundation_slot_for(&self, card: &crate::card::Card) -> Option<u8> {
|
||||
let mut candidate: Option<u8> = None;
|
||||
let mut empty_slot: Option<u8> = None;
|
||||
for slot in 0..4_u8 {
|
||||
let foundation = PileType::Foundation(slot);
|
||||
let pile = &self.piles[&foundation];
|
||||
let pile = &self.piles[&PileType::Foundation(slot)];
|
||||
if pile.cards.is_empty() {
|
||||
if empty_slot.is_none() {
|
||||
empty_slot = Some(slot);
|
||||
@@ -479,20 +497,12 @@ impl GameState {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let target_slot = candidate.or_else(|| {
|
||||
// Only fall back to an empty slot if the card is an Ace,
|
||||
// which is the only rank that can claim an empty slot.
|
||||
let target = candidate.or_else(|| {
|
||||
if card.rank.value() == 1 { empty_slot } else { None }
|
||||
});
|
||||
if let Some(slot) = target_slot {
|
||||
let foundation = PileType::Foundation(slot);
|
||||
if can_place_on_foundation(card, &self.piles[&foundation]) {
|
||||
return Some((tableau, foundation));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
target.filter(|&slot| {
|
||||
can_place_on_foundation(card, &self.piles[&PileType::Foundation(slot)])
|
||||
})
|
||||
}
|
||||
|
||||
/// Time bonus added to score on win: `700_000 / elapsed_seconds` (0 if elapsed is 0).
|
||||
@@ -1022,24 +1032,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_complete_false_when_waste_not_empty() {
|
||||
fn auto_complete_true_when_stock_empty_waste_has_cards() {
|
||||
// Waste no longer blocks auto-complete — draw() drains it during
|
||||
// auto-complete steps. Only stock-not-empty and face-down tableau
|
||||
// cards block the flag.
|
||||
let mut g = new_game();
|
||||
g.piles.get_mut(&PileType::Stock).unwrap().cards.clear();
|
||||
// Leave the waste pile untouched (it may be empty after clearing stock,
|
||||
// so add a card explicitly to ensure the waste guard is exercised).
|
||||
g.piles.get_mut(&PileType::Waste).unwrap().cards.push(Card {
|
||||
id: 99,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
});
|
||||
// Make all tableau cards face-up so only the waste guard is the blocker.
|
||||
for i in 0..7 {
|
||||
for c in g.piles.get_mut(&PileType::Tableau(i)).unwrap().cards.iter_mut() {
|
||||
c.face_up = true;
|
||||
}
|
||||
}
|
||||
assert!(!g.check_auto_complete());
|
||||
assert!(g.check_auto_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -15,6 +15,7 @@ async-trait = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
# `keyring-core` is the typed Entry/Error API used by
|
||||
# `auth_tokens`. The crate's own dependency tree pulls in
|
||||
|
||||
@@ -163,5 +163,8 @@ pub use replay::{
|
||||
REPLAY_HISTORY_CAP, REPLAY_HISTORY_SCHEMA_VERSION, REPLAY_SCHEMA_VERSION,
|
||||
};
|
||||
|
||||
pub mod matomo_client;
|
||||
pub use matomo_client::MatomoClient;
|
||||
|
||||
pub mod platform;
|
||||
pub use platform::data_dir;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
//! Matomo HTTP Tracking API client.
|
||||
//!
|
||||
//! Buffers game-play events and flushes them via the Matomo bulk tracking
|
||||
//! endpoint. Errors are silently discarded — analytics must never affect
|
||||
//! gameplay or block the UI.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use reqwest::Client;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Sends game-play events to a self-hosted Matomo instance via the
|
||||
/// [HTTP Tracking API](https://developer.matomo.org/api-reference/tracking-api).
|
||||
///
|
||||
/// Construct once per session and share via `Arc`. `event` is cheap and
|
||||
/// can be called from the Bevy main thread; `flush` is async and must be
|
||||
/// called from a background task.
|
||||
pub struct MatomoClient {
|
||||
tracking_url: String,
|
||||
site_id: u32,
|
||||
/// 16 hex-char visitor ID, stable for the lifetime of this client.
|
||||
visitor_id: String,
|
||||
uid: Option<String>,
|
||||
client: Client,
|
||||
/// Pre-encoded query strings, one per buffered event.
|
||||
pending: Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
impl MatomoClient {
|
||||
/// Create a new client targeting `base_url` (e.g. `"https://analytics.example.com"`).
|
||||
pub fn new(base_url: impl AsRef<str>, site_id: u32, uid: Option<String>) -> Self {
|
||||
let base = base_url.as_ref().trim_end_matches('/');
|
||||
let tracking_url = format!("{}/matomo.php", base);
|
||||
// Take the lower 64 bits of a v4 UUID and format as 16 hex chars.
|
||||
let visitor_id = format!("{:016x}", Uuid::new_v4().as_u128() as u64);
|
||||
Self {
|
||||
tracking_url,
|
||||
site_id,
|
||||
visitor_id,
|
||||
uid,
|
||||
client: Client::new(),
|
||||
pending: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Buffer one Matomo custom event. Never blocks; never fails visibly.
|
||||
///
|
||||
/// When the buffer exceeds 100 events the oldest 50 are dropped to
|
||||
/// prevent unbounded memory growth during extended offline play.
|
||||
pub fn event(
|
||||
&self,
|
||||
category: &str,
|
||||
action: &str,
|
||||
name: Option<&str>,
|
||||
value: Option<f64>,
|
||||
) {
|
||||
let Ok(mut guard) = self.pending.lock() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut qs = format!(
|
||||
"idsite={}&rec=1&apiv=1&send_image=0\
|
||||
&url=game%3A%2F%2Fsolitaire%2Fevent\
|
||||
&_id={}&e_c={}&e_a={}",
|
||||
self.site_id,
|
||||
self.visitor_id,
|
||||
url_encode(category),
|
||||
url_encode(action),
|
||||
);
|
||||
if let Some(n) = name {
|
||||
qs.push_str(&format!("&e_n={}", url_encode(n)));
|
||||
}
|
||||
if let Some(v) = value {
|
||||
qs.push_str(&format!("&e_v={v}"));
|
||||
}
|
||||
if let Some(uid) = &self.uid {
|
||||
qs.push_str(&format!("&uid={}", url_encode(uid)));
|
||||
}
|
||||
|
||||
guard.push(qs);
|
||||
if guard.len() > 100 {
|
||||
guard.drain(0..50);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain the pending buffer and POST it to Matomo's bulk tracking endpoint.
|
||||
///
|
||||
/// The buffer is drained *before* the HTTP call so events recorded during
|
||||
/// an in-flight flush are not lost. Network errors are silently discarded.
|
||||
pub async fn flush(&self) {
|
||||
let pending = {
|
||||
let Ok(mut guard) = self.pending.lock() else {
|
||||
return;
|
||||
};
|
||||
if guard.is_empty() {
|
||||
return;
|
||||
}
|
||||
std::mem::take(&mut *guard)
|
||||
};
|
||||
|
||||
let requests: Vec<String> = pending.into_iter().map(|qs| format!("?{qs}")).collect();
|
||||
let body = serde_json::json!({ "requests": requests });
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.post(&self.tracking_url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn url_encode(s: &str) -> String {
|
||||
s.chars()
|
||||
.flat_map(|c| match c {
|
||||
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
|
||||
vec![c]
|
||||
}
|
||||
c => format!("%{:02X}", c as u32).chars().collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -49,7 +49,7 @@ pub enum SyncBackend {
|
||||
#[default]
|
||||
#[serde(rename = "local")]
|
||||
Local,
|
||||
/// Sync with a self-hosted Solitaire Quest server.
|
||||
/// Sync with a self-hosted Ferrous Solitaire server.
|
||||
#[serde(rename = "solitaire_server")]
|
||||
SolitaireServer {
|
||||
/// Base URL of the server, e.g. `"https://solitaire.example.com"`.
|
||||
@@ -143,11 +143,10 @@ pub struct Settings {
|
||||
#[serde(default)]
|
||||
pub window_geometry: Option<WindowGeometry>,
|
||||
/// Identifier of the active card-art theme. Matches `meta.id` from
|
||||
/// the theme's `theme.ron` manifest. `"default"` is the bundled
|
||||
/// theme and is always present in the registry; user-supplied
|
||||
/// themes register under their own ids when they're imported.
|
||||
/// Older `settings.json` files default cleanly to `"default"` via
|
||||
/// `#[serde(default = ...)]`.
|
||||
/// the theme's `theme.ron` manifest. `"dark"` and `"classic"` are
|
||||
/// always present; user-supplied themes register under their own ids.
|
||||
/// Older `settings.json` files that stored `"default"` or `"classic"`
|
||||
/// are migrated to `"dark"` by [`Settings::sanitized`].
|
||||
#[serde(default = "default_theme_id")]
|
||||
pub selected_theme_id: String,
|
||||
/// Set to `true` once the achievement-onboarding info-toast has been
|
||||
@@ -243,6 +242,21 @@ pub struct Settings {
|
||||
/// `false` via `#[serde(default)]`.
|
||||
#[serde(default)]
|
||||
pub take_from_foundation: bool,
|
||||
/// When `true`, anonymous game-play events (game start, game won, etc.)
|
||||
/// are sent to the configured Matomo instance. Opt-in; defaults to `false`.
|
||||
/// Requires `matomo_url` to be set. Older `settings.json` files deserialize
|
||||
/// cleanly to `false` via `#[serde(default)]`.
|
||||
#[serde(default)]
|
||||
pub analytics_enabled: bool,
|
||||
/// Base URL of the Matomo instance to send events to, e.g.
|
||||
/// `"https://analytics.example.com"`. When `None` the analytics toggle has
|
||||
/// no effect. Older `settings.json` files deserialize cleanly to `None`.
|
||||
#[serde(default)]
|
||||
pub matomo_url: Option<String>,
|
||||
/// Matomo site ID assigned when the tracked site was created in Matomo.
|
||||
/// Defaults to `1` (the first site created in a fresh Matomo install).
|
||||
#[serde(default = "default_matomo_site_id")]
|
||||
pub matomo_site_id: u32,
|
||||
}
|
||||
|
||||
fn default_draw_mode() -> DrawMode {
|
||||
@@ -258,7 +272,7 @@ fn default_music_volume() -> f32 {
|
||||
}
|
||||
|
||||
fn default_theme_id() -> String {
|
||||
"default".to_string()
|
||||
"dark".to_string()
|
||||
}
|
||||
|
||||
/// Default tooltip-hover dwell delay in seconds. Mirrors
|
||||
@@ -311,6 +325,10 @@ fn default_replay_move_interval_secs() -> f32 {
|
||||
0.45
|
||||
}
|
||||
|
||||
fn default_matomo_site_id() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// Lower bound of the player-tunable replay-playback per-move interval,
|
||||
/// in seconds. Below this the cards barely register visually before
|
||||
/// the next move fires; the cap keeps the playback legible.
|
||||
@@ -364,6 +382,9 @@ impl Default for Settings {
|
||||
last_difficulty: None,
|
||||
leaderboard_display_name: None,
|
||||
take_from_foundation: false,
|
||||
analytics_enabled: false,
|
||||
matomo_url: None,
|
||||
matomo_site_id: default_matomo_site_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,6 +395,13 @@ impl Settings {
|
||||
/// their respective ranges after deserialization or hand-editing of
|
||||
/// `settings.json`.
|
||||
pub fn sanitized(self) -> Self {
|
||||
// Migrate stale theme IDs: "default" was removed when the theme was
|
||||
// renamed to "dark"; "classic" was briefly the default before "dark"
|
||||
// was restored as the shipped default.
|
||||
let selected_theme_id = match self.selected_theme_id.as_str() {
|
||||
"default" | "classic" => "dark".to_string(),
|
||||
_ => self.selected_theme_id,
|
||||
};
|
||||
Self {
|
||||
sfx_volume: self.sfx_volume.clamp(0.0, 1.0),
|
||||
music_volume: self.music_volume.clamp(0.0, 1.0),
|
||||
@@ -386,6 +414,7 @@ impl Settings {
|
||||
replay_move_interval_secs: self
|
||||
.replay_move_interval_secs
|
||||
.clamp(REPLAY_MOVE_INTERVAL_MIN_SECS, REPLAY_MOVE_INTERVAL_MAX_SECS),
|
||||
selected_theme_id,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! | Struct | Backend |
|
||||
//! |---|---|
|
||||
//! | [`LocalOnlyProvider`] | No-op; used when sync is disabled |
|
||||
//! | [`SolitaireServerClient`] | Self-hosted Solitaire Quest server (JWT auth) |
|
||||
//! | [`SolitaireServerClient`] | Self-hosted Ferrous Solitaire server (JWT auth) |
|
||||
//!
|
||||
//! Use [`provider_for_backend`] to obtain a `Box<dyn SyncProvider + Send + Sync>`
|
||||
//! without matching on [`SyncBackend`] anywhere else in the codebase.
|
||||
@@ -55,7 +55,7 @@ impl SyncProvider for LocalOnlyProvider {
|
||||
// SolitaireServerClient
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// HTTP sync client for the self-hosted Solitaire Quest server.
|
||||
/// HTTP sync client for the self-hosted Ferrous Solitaire server.
|
||||
///
|
||||
/// Authenticates via JWT stored in the OS keychain. On a 401 response the
|
||||
/// client automatically attempts a token refresh and retries the request once
|
||||
|
||||
@@ -14,6 +14,7 @@ chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
resvg = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<defs>
|
||||
<pattern id="dp" x="0" y="0" width="28" height="28" patternUnits="userSpaceOnUse">
|
||||
<rect width="28" height="28" fill="#1a3a6e"/>
|
||||
<polygon points="14,2 26,14 14,26 2,14" fill="#2255aa"/>
|
||||
<polygon points="14,7 21,14 14,21 7,14" fill="#1a3a6e"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<!-- White card background -->
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14" fill="#FAFAF8"/>
|
||||
<!-- Red outer border -->
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="none" stroke="#CC1111" stroke-width="4"/>
|
||||
<!-- Navy diamond pattern inset -->
|
||||
<rect x="16" y="16" width="224" height="352" rx="8" ry="8" fill="url(#dp)"/>
|
||||
<!-- Thin red frame around pattern -->
|
||||
<rect x="16" y="16" width="224" height="352" rx="8" ry="8"
|
||||
fill="none" stroke="#CC1111" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 924 B |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 13,4 10,7 10,10 C 10,12 11,13 12,14 C 9,14 4,17 4,21 C 4,24 7,27 10,27 C 12,27 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 20,27 22,27 C 25,27 28,24 28,21 C 28,17 23,14 20,14 C 21,13 22,12 22,10 C 22,7 19,4 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,2 L 30,16 L 16,30 L 2,16 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#CC1111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,28 C 8,22 2,17 2,11 C 2,7 5,4 9,4 C 12,4 14,6 16,9 C 18,6 20,4 23,4 C 27,4 30,7 30,11 C 30,17 24,22 16,28 Z" fill="#CC1111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="11" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">10</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">2</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">3</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">4</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">5</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">6</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">7</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">8</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">9</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">A</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">J</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">K</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384" viewBox="0 0 256 384">
|
||||
<rect x="2" y="2" width="252" height="380" rx="14" ry="14"
|
||||
fill="#FAFAF8" stroke="#AAAAAA" stroke-width="1.5"/>
|
||||
|
||||
<!-- Top-left corner: rank label + small suit -->
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Centre: large suit, 64x64 in 256x384 card -->
|
||||
<g transform="translate(96 160) scale(2)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
|
||||
<!-- Bottom-right corner: mirrored via 180° rotation around card centre -->
|
||||
<g transform="rotate(180 128 192)">
|
||||
<text x="14" y="44" font-family="Fira Mono" font-size="36" font-weight="700"
|
||||
fill="#111111">Q</text>
|
||||
<g transform="translate(14 50) scale(0.625)">
|
||||
<path d="M16,4 C 9,9 2,14 2,21 C 2,25 5,28 9,28 C 13,28 14,26 14,24 L 13,30 L 19,30 L 18,24 C 18,26 19,28 23,28 C 27,28 30,25 30,21 C 30,14 23,9 16,4 Z" fill="#111111"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,64 @@
|
||||
(
|
||||
meta: (
|
||||
id: "classic",
|
||||
name: "Classic",
|
||||
author: "Ferrous Solitaire",
|
||||
version: "1.0.0",
|
||||
card_aspect: (2, 3),
|
||||
),
|
||||
back: "back.svg",
|
||||
faces: {
|
||||
"clubs_ace": "clubs_ace.svg",
|
||||
"clubs_2": "clubs_2.svg",
|
||||
"clubs_3": "clubs_3.svg",
|
||||
"clubs_4": "clubs_4.svg",
|
||||
"clubs_5": "clubs_5.svg",
|
||||
"clubs_6": "clubs_6.svg",
|
||||
"clubs_7": "clubs_7.svg",
|
||||
"clubs_8": "clubs_8.svg",
|
||||
"clubs_9": "clubs_9.svg",
|
||||
"clubs_10": "clubs_10.svg",
|
||||
"clubs_jack": "clubs_jack.svg",
|
||||
"clubs_queen": "clubs_queen.svg",
|
||||
"clubs_king": "clubs_king.svg",
|
||||
"diamonds_ace": "diamonds_ace.svg",
|
||||
"diamonds_2": "diamonds_2.svg",
|
||||
"diamonds_3": "diamonds_3.svg",
|
||||
"diamonds_4": "diamonds_4.svg",
|
||||
"diamonds_5": "diamonds_5.svg",
|
||||
"diamonds_6": "diamonds_6.svg",
|
||||
"diamonds_7": "diamonds_7.svg",
|
||||
"diamonds_8": "diamonds_8.svg",
|
||||
"diamonds_9": "diamonds_9.svg",
|
||||
"diamonds_10": "diamonds_10.svg",
|
||||
"diamonds_jack": "diamonds_jack.svg",
|
||||
"diamonds_queen": "diamonds_queen.svg",
|
||||
"diamonds_king": "diamonds_king.svg",
|
||||
"hearts_ace": "hearts_ace.svg",
|
||||
"hearts_2": "hearts_2.svg",
|
||||
"hearts_3": "hearts_3.svg",
|
||||
"hearts_4": "hearts_4.svg",
|
||||
"hearts_5": "hearts_5.svg",
|
||||
"hearts_6": "hearts_6.svg",
|
||||
"hearts_7": "hearts_7.svg",
|
||||
"hearts_8": "hearts_8.svg",
|
||||
"hearts_9": "hearts_9.svg",
|
||||
"hearts_10": "hearts_10.svg",
|
||||
"hearts_jack": "hearts_jack.svg",
|
||||
"hearts_queen": "hearts_queen.svg",
|
||||
"hearts_king": "hearts_king.svg",
|
||||
"spades_ace": "spades_ace.svg",
|
||||
"spades_2": "spades_2.svg",
|
||||
"spades_3": "spades_3.svg",
|
||||
"spades_4": "spades_4.svg",
|
||||
"spades_5": "spades_5.svg",
|
||||
"spades_6": "spades_6.svg",
|
||||
"spades_7": "spades_7.svg",
|
||||
"spades_8": "spades_8.svg",
|
||||
"spades_9": "spades_9.svg",
|
||||
"spades_10": "spades_10.svg",
|
||||
"spades_jack": "spades_jack.svg",
|
||||
"spades_queen": "spades_queen.svg",
|
||||
"spades_king": "spades_king.svg",
|
||||
},
|
||||
)
|
||||
|
Before Width: | Height: | Size: 956 B After Width: | Height: | Size: 956 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |