Geography-themed Klondike-style solitaire for iPhone and iPad
- Xcode 15+ (for iOS 16+ deployment target)
- macOS Ventura or later
- Swift 5.9+
This repository contains all source files but no .xcodeproj file. Follow these steps to create a working Xcode project:
- Open Xcode and select File → New → Project
- Choose iOS → App template
- Configure:
- Product Name:
AtlasSolitaire - Team: (your Apple Developer team)
- Organization Identifier:
com.yourcompany(or leave default) - Interface: SwiftUI
- Language: Swift
- Storage: None (we use custom persistence)
- Uncheck "Include Tests" (we have custom test targets)
- Product Name:
- Save the project inside
/Users/johnhawley/Documents/src/AtlasSolitaire/
- Delete the default
ContentView.swiftand any placeholder files Xcode created - Drag and drop the following folders from Finder into the Xcode project navigator:
App/Sources/Data/Tests/(create a new test target first if needed)
- When prompted, select:
- ✅ Copy items if needed (uncheck — files are already in place)
- ✅ Create groups (not folder references)
- ✅ Add to target:
AtlasSolitaire
- For the
Data/folder:- Make sure both
groups/anddecks/subfolders are added to the app target - Verify JSON files appear in Build Phases → Copy Bundle Resources
- Make sure both
- Select a simulator (e.g., iPhone 15 Pro) or device
- Press ⌘R to build and run
- You should see the Atlas Solitaire menu screen
AtlasSolitaire/
├─ App/
│ └─ AtlasSolitaireApp.swift # App entry point, view model wiring
├─ Sources/
│ ├─ Models/ # Data models (Card, Group, Deck, GameState)
│ ├─ GameEngine/ # Core game logic (Rules, GameEngine)
│ ├─ ViewModels/ # GameViewModel (ObservableObject)
│ ├─ Views/ # SwiftUI views (CardView, GameView, etc.)
│ ├─ Services/ # Managers (DeckManager, Persistence, Audio, Haptics)
│ └─ Utils/ # Extensions and shared constants
├─ Data/
│ ├─ groups/ # JSON group definitions (5 samples included)
│ └─ decks/ # Deck definition files
├─ Tests/ # Unit tests (GameEngineTests, DeckManagerTests)
├─ PROJECT_SPEC.md # Full feature specification
└─ README.md # This file
Complete all geography card groups by stacking base cards and their partners onto foundation piles.
- Stock pile (top-right): Tap to draw cards one at a time to the waste pile. When empty, tap again to reshuffle.
- Waste pile (top-left): The top card is always visible and draggable.
- Foundation piles (4 slots):
- Place a base card (e.g., "Countries of Europe") on an empty slot.
- Stack partner cards from the same group (e.g., "France", "Italy") onto the base in any order.
- When all cards of a group are on a foundation, the group is cleared (removed from play).
- Tableau piles (4 columns):
- Cards are dealt face-down; top card is face-up.
- Empty tableau slots accept any card.
- Partner cards from the same group can stack on each other in tableau.
- Win condition: Clear all groups in the deck.
- Tap-to-select: Tap a card to select it (highlighted in gold), then tap a destination.
- Drag-and-drop: Drag a card directly to a foundation or tableau pile.
- Tap stock: Draw a card (or reshuffle when stock is empty).
- GameViewModel is the single source of truth, owns the
GameEngine, and publishes state to SwiftUI views. - GameEngine contains pure game logic (no SwiftUI) for easy unit testing.
- Rules module is fully stateless — all validation logic is static and deterministic.
- User interaction (tap / drag) → View calls intent method on GameViewModel
- GameViewModel calls GameEngine mutation method
- GameEngine validates via Rules, mutates
GameState, triggersonStateChangedcallback - GameViewModel publishes updated state → SwiftUI re-renders
- Game state auto-saves to Application Support directory as JSON after every move.
- On app launch, the last saved game is restored (if
phase == .playing). - Settings (sound/haptics toggles) persist separately.
- AudioManager pre-loads
.wavfiles fromAssets/Sounds/(placeholders for now). - HapticManager fires
UIImpactFeedbackGeneratorevents on drag/drop/success. - Both managers respect user settings.
- Create a new JSON file in
Data/groups/following this format:
{
"group_id": "african_capitals_01",
"group_name": "African Capitals",
"base_card": {
"id": "african_capitals_base",
"label": "African Capitals",
"type": "base",
"image": null
},
"partner_cards": [
{ "id": "cairo", "label": "Cairo", "type": "partner", "image": null },
{ "id": "nairobi", "label": "Nairobi", "type": "partner", "image": null }
],
"metadata": {
"difficulty": "medium",
"source": "local-json"
}
}- Add the file to the Xcode project (Copy Bundle Resources)
- The
DeckManagerwill automatically discover it when building random decks
Note: Card IDs within a group must be unique (e.g., "cairo"). IDs are scoped at runtime by prefixing with group_id to ensure global uniqueness across groups.
Run tests via ⌘U in Xcode or:
xcodebuild test -scheme AtlasSolitaire -destination 'platform=iOS Simulator,name=iPhone 15 Pro'Coverage includes:
- All
Rulesvalidation logic (foundation, tableau, reshuffle, group completion) GameEngineoperations (draw, move, reshuffle, win detection)DeckManagergroup loading, deduplication, seeded shuffles
- New game starts with 4 tableau piles, cards dealt face-down except top cards
- Tapping stock draws a card to waste
- Reshuffle works when stock is empty
- Base cards can only be placed on empty foundations
- Partner cards stack on matching bases
- Completed groups are cleared and foundation slot becomes available
- Win screen appears when all groups cleared
- Game state persists across app restarts
- Sound/haptic settings persist
The DeckManager uses a protocol GroupDataSource. To fetch groups from a server:
- Create a new class
RemoteAPIDataSource: GroupDataSource - Implement
loadAllGroups()andloadDeck(id:)with network calls - Inject
RemoteAPIDataSource()intoDeckManagerat app launch
The rest of the codebase remains unchanged.
- Add
.pngassets toAssets.xcassets(e.g.,france.png) - Update JSON files with
"image": "france" - Modify
CardViewto displayImage(card.imageName!)instead of text labels
The GameState has a @CodableIgnored var previousState field ready for single-level undo. Wire up:
engine.state.previousState = currentStateCopybefore mutations, then add an "Undo" button that calls:
if let prev = engine.state.previousState {
engine.state = prev
publishState()
}- No hint system — player must discover valid moves manually
- No animations for group clear — cards disappear instantly (opportunity for particle effects)
- No difficulty settings — number of groups per round is hardcoded (easy to parameterize)
- No statistics / leaderboards — no game history tracking yet
- Sound files are placeholders — actual
.wavassets not included
Built in Swift + SwiftUI following Apple's modern best practices.
Architecture: MVVM with ObservableObject
Persistence: Codable JSON to Application Support
Animations: SwiftUI .animation() and matchedGeometryEffect
Haptics: UIKit UIImpactFeedbackGenerator
Audio: AVFoundation AVAudioPlayer
This project is provided as-is for educational and development purposes. Customize freely.
Enjoy your journey around the world, one card at a time! 🌍🃏