A native macOS clone of Windows XP Spider Solitaire, written in Swift and SwiftUI.
- The rules and UI are described in SPEC.md.
- It builds, tests and packages on macOS 14; the rules engine also builds and tests on Linux.
- In English and German; it follows the macOS language (SPEC.md §3.8 explains how to add one).
- Website: https://dansailer.github.io/SpiderSolitaire/ · Downloads: https://github.com/dansailer/SpiderSolitaire/releases/latest
- Code: MIT (LICENSE). Card art: CC0 / public domain (CREDITS.md).
- Outside contributions are not accepted (CONTRIBUTING.md); report security issues privately (SECURITY.md).
- macOS 14 (Sonoma) or later
- Xcode 15 or later (it includes Swift 5.9 or later), or the matching Command Line Tools
cd spider-solitaire
swift build # builds SpiderCore and the app
swift run SpiderSolitaire # launches the game window
swift test # runs the SpiderCore XCTest suiteIf swift run opens a window that won't take keyboard focus, click it once.
./scripts/package.sh # or: bash scripts/package.sh
open "build/Spider Solitaire.app"The script (macOS only):
- Runs
swift build -c release. - Renders the icon in code (
swift run SpiderIcon, CoreGraphics) and turns it intoAppIcon.icnswithsipsandiconutil. - Assembles
build/Spider Solitaire.appfromPackaging/Info.plist(bundle idcom.dansailer.SpiderSolitaire, minimum macOS 14). - Signs it, inside out without
--deep(ad hoc by default, or withSIGN_IDENTITY), and zips it tobuild/SpiderSolitaire.zip.
Options: VERSION=1.2.0 sets the version (default 1.0.0), BUILD_NUMBER defaults to the git commit
count, and UNIVERSAL=1 builds arm64 + x86_64 (needs full Xcode).
SIGN_IDENTITY="Developer ID Application: Daniel Sailer (595TEE9LSF)" signs with that keychain identity,
hardened runtime and a secure timestamp (SIGN_KEYCHAIN limits the lookup to one keychain); without
it the app is ad-hoc signed, and on another Mac it has to be opened with right-click ▸ Open (or
System Settings ▸ Privacy & Security ▸ Open Anyway) the first time.
Releases after 1.0.0 are Developer ID signed, notarized by Apple and stapled by the Release workflow, so they open with a double click. v1.0.0 was ad-hoc signed and still needs right-click ▸ Open the first time.
The script also copies SwiftPM's resource bundle (SpiderSolitaire_SpiderSolitaire.bundle, which holds the
Classic card faces) into Contents/Resources in the standard bundle layout, so the app works outside the
build folder.
swift run SpiderIcon icon.svg # app icon (on a Mac, icon.png renders with CoreGraphics)
swift run SpiderIcon --card-back back.svg # card back at 120 x 162 pt
swift run SpiderIcon --badges badges/ # every achievement medal, locked and unlocked
python3 scripts/cards/preview_faces.py faces.pngThe icon and card back are described once as data in Sources/SpiderBoard/VectorArt.swift. SwiftUI,
CoreGraphics and SVG all draw from that description. The back has a gold rosette and the icon has a
spade card; there are no spider motifs anywhere.
The Classic faces come from Dmitry Fomin's "English pattern" deck on Wikimedia Commons (CC0). That deck
draws every suit pip into a tall 2:3 box, so the build script rescales each pip about its centre to the
usual proportions (hearts about 1:1). The faces ship as vector PDFs at the source's 2:3 page size
(360 x 540 pt). The app renders each PDF once into a bitmap of the card's exact pixel size (card size in
points times the display's backing scale), fits the art onto the 71:96 card with one uniform scale, and
draws that bitmap 1:1, so the faces stay sharp at every window size and on every display. The bitmaps are
cached per card and pixel size (CardBitmapCache), so dragging and the win animation only copy pixels. The ace
of spades also uses the ornate centre spade from Byron Knoll's public-domain deck, kept as a vector path.
To regenerate the PDFs you need rsvg-convert (brew install librsvg) and the Python package
svgelements; run python3 scripts/cards/build_classic_cards.py. It checks
each file page's licence and writes Resources/Cards/CREDITS.md.
CI: .github/workflows/ci.yml runs the tests on Linux and macOS 14 and uploads the zip.
open Package.swift # or: xed .Then:
- Pick the SpiderSolitaire scheme and My Mac as the destination.
- Press ⌘R to run or ⌘U to run the tests.
Xcode opens the Swift package directly, so no .xcodeproj is needed.
- Drag any face-up card that starts a same-suit descending run (the run comes with it) onto a card one rank higher, or onto an empty column. An illegal drop slides the cards back.
- Click a card to send its run to the best legal column: a same-suit card one rank higher first, then any card one rank higher, then an empty column. A card with nowhere to go stays put.
- Click the stock (bottom right) or choose Game ▸ Deal Next Row (⌘D) to deal a row.
- Click the score box, or Game ▸ Show Hint (⌘T), to flash the next available move. Repeat to cycle.
- Settings… (⌘,) has animations, sounds (off by default; macOS system sounds), Card faces: Classic / Simple, Only deal winnable games, the undo mode and the save/open-at-launch rules. Changes apply at once.
- Only deal winnable games: new games use deals a built-in solver has proved winnable. Usually instant; otherwise "Dealing a winnable game…" appears for up to 5 s (Cancel deals a random game).
- Achievements (Game ▸ Achievements…, ⇧⌘A): 14 medals, listed with their thresholds in SPEC.md §7. A banner appears at the top of the board when one unlocks. The score box shows the game's play time.
- After a win, the cards cascade off the foundation with fireworks, then a summary shows your score, time, moves, streaks and new achievements. Click or press any key to skip straight to it.
| Menu | Item | Shortcut |
|---|---|---|
| Spider Solitaire | About Spider Solitaire, Check for Updates…, Settings… (⌘,), Services, Hide (⌘H), Hide Others (⌥⌘H), Show All, Quit (⌘Q) | |
| Game | New Game | ⌘N |
| Restart Game | ⇧⌘N | |
| Undo / Redo | ⌘Z / ⇧⌘Z | |
| Deal Next Row | ⌘D | |
| Show Hint | ⌘T | |
| Difficulty ▸ One Suit / Two Suits / Four Suits | ⌘1 / ⌘2 / ⌘4 | |
| Statistics… | ⌘I | |
| Achievements… | ⇧⌘A | |
| Save Game / Open Saved Game | ⌘S / ⌘O | |
| Close Window | ⌘W | |
| Window | Minimize, Zoom, Enter Full Screen | ⌘M, –, ⌃⌘F |
| Help | Spider Solitaire Help: rules, scoring, tips, achievements and every shortcut | ⌘? |
| Visit Website (https://dansailer.github.io/SpiderSolitaire/) |
There is no File, Edit or View menu and no window tabs. SPEC.md §3.5 explains each choice.
Spider Solitaire ▸ Check for Updates… asks GitHub's public API for the latest release
(no account, no token, nothing else is sent) and compares it with the running version. If there is
a newer one, Install downloads it with a progress bar and checks it: the zip's SHA-256 against the
release's .sha256 file, and the app's signature (it must be signed with this project's Developer ID
and be a newer version). Then choose Restart to Update, which quits, swaps in the new version (keeping
the old one until the new one is in place) and reopens it, or Later, which keeps your version and
deletes the download until the next check. If the app can't replace itself (for example it runs
from Downloads, where macOS opens it from a temporary read-only location, or its folder isn't
writable), Install saves SpiderSolitaire.zip to Downloads and shows it in Finder instead. When you
open it from Downloads, it offers once to move itself to Applications. With "Check for updates
automatically" on (Settings ▸ General, the default) it checks at launch, at most once a day, and
stays silent unless an update exists.
Work happens on branches; main only changes through pull requests whose checks passed. The cycle
(details in CONTRIBUTING.md):
- Branch from
main(feature/…,fix/…,ci/…), commit, runswift testandscripts/lint.sh. - Push and open a pull request into
main. - The required checks run: Linux tests, macOS build and tests, SwiftLint,
swift-format and jactionlint. The
protect-mainruleset blocks the merge until all are green and the branch is up to date withmain; direct pushes tomainare rejected. - Merge (squash, merge commit or rebase); the branch is deleted.
- To release, tag the merged
main(the tag is the version), which runs the Release workflow.
Workflows only run in dansailer/SpiderSolitaire: for dansailer's pull requests from branches of
this repository, v* tags, Pages deploys and manual dispatches (CI does not rerun on main after a
merge; the up-to-date pull request was already checked), and for Dependabot's pull
requests (the required checks run on them too). They use pull_request (never
pull_request_target), so pull request runs get a read-only token and no secrets; fork pull
requests also need approval before anything runs. Every action is pinned to a commit SHA, each job
gets only the permissions it needs, and checkouts do not keep credentials.
| Workflow | Does |
|---|---|
ci.yml |
Linux swift test in a pinned container; macOS swift test plus scripts/package.sh; SwiftLint (pinned, checksum-verified release binary); swift format lint --strict in a pinned Swift 6.3 container |
jactionlint.yml |
Lints the workflows and dependabot.yml with jactionlint, pedantic profile, failing on any finding |
release.yml |
On a v* tag on main or a release/x.y branch, in the release environment: tests, a universal build signed with the Developer ID (scripts/signing-keychain.sh, scripts/package.sh), notarized and stapled (scripts/notarize.sh), then a GitHub release with SpiderSolitaire.zip, its .sha256 and generated notes (a tag with - becomes a prerelease) |
pages.yml |
Deploys docs/ to GitHub Pages on pushes to main that change it (Settings ▸ Pages ▸ Source must be "GitHub Actions") |
Cut a release from an up to date main after the pull request is merged:
git switch main && git pull --ff-only
git tag -a v1.1.0 -m "Spider Solitaire 1.1.0"
git push origin v1.1.0If the run fails after the tag exists, fix it and rerun it with the tag as the ref, which the
release environment requires: gh workflow run release.yml --ref v1.1.0 -f tag=v1.1.0 (the
workflow builds the tagged commit). Tags are never moved or reused.
Release builds are signed with a Developer ID certificate and notarized by Apple, using secrets that
only the release environment (deployable from v* tags only) can read; no pull request job sees them.
Lint locally with scripts/lint.sh (--fix formats and applies SwiftLint's fixes first): it runs
swift format from the toolchain with .swift-format and SwiftLint with .swiftlint.yml, which
scripts/install-swiftlint.sh downloads to .build/tools (pinned version, SHA-256 checked). Lint the
workflows with pipx run jactionlint --profile pedantic (or pip install jactionlint).
The website in docs/ is static HTML, CSS and a little vanilla JavaScript with no build step and no
third party requests; fonts (Cormorant Garamond, Manrope, SIL OFL) and Phosphor icons are committed.
Its screenshots come from the app itself:
scripts/package.sh && "build/Spider Solitaire.app/Contents/MacOS/SpiderSolitaire" --snapshot /tmp/snaps
python3 scripts/site_assets.py /tmp/snaps <icon.svg> <back.svg> <fontsource dir> <phosphor dir>--snapshot <dir> uses a temporary store and a fixed two suit deal, plays 38 solver moves and writes
PNGs of the board, help, achievements and every Settings tab (no screen recording permission needed).
--snapshot <dir> --win-frames plays that deal to its win and writes frames of the win animation and panel.
--menu-probe <seconds> plays moves while checking that the menu bar never changes (exit 0 if so).
swift build -c release --product SpiderTools
.build/release/SpiderTools bench 2 50 # success rate and time per seed, as the app runs it
.build/release/SpiderTools find 4 20 # time to find one winnable deal
.build/release/SpiderTools seeds 1 24 # generate verified seeds for WinnableDeals.bundled
.build/release/SpiderTools help # the help window's text as Markdownpython3 scripts/check_builders.py flags any SwiftUI builder block with more than 10 direct
children (result builders past 10 items fail to compile, and Linux can't compile the app target).
SpiderCore and SpiderBoard use only the Swift standard library, so they build and test anywhere Swift runs:
swift build --target SpiderCore
swift testOn Linux, the app target compiles to a small console stub, because SwiftUI is not available there.
Package.swift
Sources/SpiderCore/ rules engine (Card, Deck, GameState, SpiderGame, Hints, Statistics)
Sources/SpiderBoard/ pure-Swift board geometry and pointer handling (BoardLayout, PointerController)
Sources/SpiderPersistence/ JSON files in Application Support (SpiderStore)
Sources/SpiderSolitaire/ SwiftUI views in an AppKit app (app delegate, windows and menu bar, GameModel, BoardView, CardView, WinCelebrationView)
Sources/SpiderIcon/ renders VectorArt (app icon, card back, badges) to PNG (CoreGraphics) or SVG (any platform)
Sources/SpiderTools/ dev-only solver tool: bench, find, seeds, bundled (not part of the app)
Sources/SpiderSolitaire/Resources/Cards/ Classic card faces (52 vector PDFs, CC0/public domain, see CREDITS.md)
Sources/SpiderSolitaire/Resources/Localization/ en.lproj and de.lproj string tables (SPEC.md §3.8)
scripts/cards/ build_classic_cards.py (download, licence check, pip fix, PDF), preview.py, preview_faces.py
Packaging/Info.plist app bundle Info.plist template
scripts/package.sh builds, signs and zips build/Spider Solitaire.app
scripts/lint.sh swift-format and SwiftLint (.swift-format, .swiftlint.yml), as in CI
scripts/install-swiftlint.sh downloads the pinned, checksum-verified SwiftLint to .build/tools
Tests/SpiderCoreTests/ XCTest suite for the rules
Tests/SpiderBoardTests/ XCTest suite for layout, hit-testing, drag/drop and click-to-move
Tests/SpiderPersistenceTests/ XCTest suite for saving, loading and quit/resume
Tests/SpiderLocalizationTests/ XCTest suite for the string tables (complete, consistent, nothing hardcoded)
SPEC.md