refactor: clean template repo — move SoliCards to its own repo
Moved all SoliCards app code to a dedicated repository: https://git.istratai.cloud/aj/SoliCards-iOS-iPadOS-MacOS This repo is now a clean workflow template with just PROMPT.md, CLAUDE.md, and README.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
@ -1,81 +0,0 @@
|
|||
# App Store Connect Metadata — SoliCards
|
||||
|
||||
## App Information
|
||||
- **App Name:** SoliCards
|
||||
- **Subtitle:** Classic Solitaire Collection
|
||||
- **Bundle ID:** com.solicards.app
|
||||
- **SKU:** SOLICARDS001
|
||||
- **Category:** Games > Card
|
||||
- **Content Rating:** 4+ (No objectionable content)
|
||||
- **Price:** Free (or set pricing)
|
||||
|
||||
## Version 1.0.0
|
||||
- **Build Number:** 1
|
||||
- **What's New:** Initial release
|
||||
|
||||
## Description
|
||||
SoliCards brings three classic solitaire card games to your iPhone, iPad, and Mac in one beautiful app.
|
||||
|
||||
Play Klondike, Spider, and FreeCell with four difficulty levels, six stunning themes, and full drag-and-drop gameplay. Every game auto-saves so you can pick up right where you left off.
|
||||
|
||||
Features:
|
||||
- Three complete solitaire variants: Klondike, Spider, and FreeCell
|
||||
- Four difficulty levels: Easy, Medium, Hard, Expert
|
||||
- Six color themes with light and dark mode support
|
||||
- 12 card back designs and multiple card face styles
|
||||
- Smart hint system and auto-complete
|
||||
- Unlimited undo (Easy) or limited undo for a challenge
|
||||
- Game statistics tracking per variant and difficulty
|
||||
- Full VoiceOver accessibility support
|
||||
- Keyboard shortcuts for Mac
|
||||
- No ads, no in-app purchases, no internet required
|
||||
|
||||
## Keywords
|
||||
solitaire, klondike, spider, freecell, card game, patience, cards, classic, offline, free cell
|
||||
|
||||
## Privacy Policy URL
|
||||
[Placeholder — required before submission]
|
||||
|
||||
## Support URL
|
||||
[Placeholder — required before submission]
|
||||
|
||||
## Screenshots Required
|
||||
|
||||
### iPhone 6.9" (iPhone 16 Pro Max)
|
||||
- 1320 x 2868 or 2868 x 1320
|
||||
- Minimum 3 screenshots
|
||||
|
||||
### iPhone 6.3" (iPhone 16 Pro)
|
||||
- 1206 x 2622 or 2622 x 1206
|
||||
|
||||
### iPad Pro 13" (M4)
|
||||
- 2064 x 2752 or 2752 x 2064
|
||||
|
||||
### Mac
|
||||
- 1280 x 800 minimum, 2560 x 1600 recommended
|
||||
|
||||
### Suggested Screenshots
|
||||
1. Klondike game in progress (Classic Green theme)
|
||||
2. Spider game showing 10-column layout
|
||||
3. FreeCell game with power moves
|
||||
4. Theme picker showing all 6 themes
|
||||
5. New Game sheet with difficulty options
|
||||
|
||||
## Age Rating Questionnaire
|
||||
- Unrestricted Web Access: No
|
||||
- Gambling/Contests: No (solitaire is single-player, no real money)
|
||||
- Mature/Suggestive Themes: None
|
||||
- Violence: None
|
||||
- All other categories: None
|
||||
- **Result: 4+**
|
||||
|
||||
## In-App Purchases
|
||||
None
|
||||
|
||||
## App Tracking Transparency
|
||||
Not required — no tracking, no third-party SDKs
|
||||
|
||||
## Export Compliance
|
||||
Uses no encryption beyond standard HTTPS (N/A — no networking)
|
||||
- **ECCN:** Not applicable
|
||||
- **Contains encryption:** No
|
||||
102
ARCHITECTURE.md
|
|
@ -1,102 +0,0 @@
|
|||
# ARCHITECTURE.md — SoliCards
|
||||
|
||||
## Overview
|
||||
|
||||
SoliCards is a native SwiftUI solitaire app supporting three game variants (Klondike, Spider, FreeCell) across iOS, iPadOS, and macOS from a single codebase. It was ported from a 2,843-line vanilla JavaScript web app to native Swift.
|
||||
|
||||
## Pattern: MVVM + Protocol-Oriented Strategy
|
||||
|
||||
### Why MVVM
|
||||
SwiftUI's `@Observable` macro (iOS 17+) gives property-level observation — a score change does not re-render the tableau. This is critical for a card game with many rapidly-changing UI elements during drag operations.
|
||||
|
||||
### Why Strategy Pattern for Variants
|
||||
The three variants share concepts (cards, foundations, tableaus, undo, scoring) but have radically different rules. A single `GameRules` protocol defines the contract:
|
||||
|
||||
```
|
||||
GameRules (protocol)
|
||||
├── KlondikeRules — alternating colors, draw 1/3, foundation A→K
|
||||
├── SpiderRules — same-suit sequences, 2-deck, auto-complete K→A
|
||||
└── FreeCellRules — power moves: (1+freecells) × 2^emptyTableaus
|
||||
```
|
||||
|
||||
Adding a new variant means implementing one struct + one board view. Zero changes to existing code.
|
||||
|
||||
### Why Not TCA
|
||||
The game has no networking, no side effects beyond sound and persistence, and all state mutations are synchronous UI gestures. TCA's unidirectional architecture would add overhead without benefit.
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
### 1. DragGesture, not onDrag/onDrop
|
||||
SwiftUI's `onDrag`/`onDrop` uses the `Transferable` protocol, which serializes data through the system pasteboard. This is designed for inter-app drag and drop. For internal card movement:
|
||||
- `DragGesture` gives direct control over positioning and animation
|
||||
- `DropTargetPreferenceKey` reports frame geometry for hit-testing
|
||||
- `DraggedCardsOverlay` renders floating cards in the board's coordinate space
|
||||
- No serialization overhead
|
||||
|
||||
### 2. @Observable, not ObservableObject
|
||||
The `@Observable` macro (Observation framework) was chosen over `ObservableObject`/`@Published` because:
|
||||
- Property-level observation — only views reading a changed property re-render
|
||||
- No `@Published` boilerplate
|
||||
- Works with `@Bindable` for two-way bindings
|
||||
- Cleaner nested observable objects
|
||||
|
||||
### 3. @MainActor on ViewModels, not actors for game logic
|
||||
All game state mutations are triggered by user gestures on the main thread. Using an actor would force every game action to be `async`, adding `await` noise for no concurrency benefit. Only `SoundManager` uses async for off-main-thread audio loading.
|
||||
|
||||
### 4. Value-type GameSnapshot for undo
|
||||
Undo is implemented via an array of `GameSnapshot` structs (Codable value types). Each snapshot is a deep copy of all card arrays, score, and moves. This avoids reference-type aliasing bugs that would occur with class-based state. The history caps at 20 entries.
|
||||
|
||||
### 5. PNG images from asset catalog, not Canvas drawing
|
||||
The source project already had 52 card front PNGs in multiple styles. Re-drawing cards procedurally would be wasted effort. Asset catalog images are:
|
||||
- Memory-mapped by the system (efficient loading)
|
||||
- Automatically thinned for App Store builds
|
||||
- Validated at compile time
|
||||
|
||||
### 6. Debounced auto-save
|
||||
Game state is saved via SwiftData after 2 seconds of inactivity (not on every move). This prevents excessive disk writes during rapid play. Immediate save occurs on app background and pause.
|
||||
|
||||
### 7. trailingSuffix(while:) extension
|
||||
Swift's standard library does not include `suffix(while:)` on `BidirectionalCollection`. Rather than adding a dependency (Swift Algorithms), a small `trailingSuffix(while:)` extension on `Array` provides the same functionality for finding face-up card runs.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User Gesture (tap / drag)
|
||||
│
|
||||
▼
|
||||
CardStackView / CardView
|
||||
│ DragGesture / onTapGesture
|
||||
▼
|
||||
GameViewModel
|
||||
│ rules.canMove() → validation
|
||||
│ state.pushHistory() → undo stack
|
||||
│ state mutation → @Observable triggers SwiftUI diff
|
||||
│ soundManager.play() → audio feedback
|
||||
│ rules.isWon() → phase transition
|
||||
│ scheduleAutoSave() → debounced SwiftData write
|
||||
▼
|
||||
GameBoardView re-renders only changed cards
|
||||
```
|
||||
|
||||
## Persistence Model
|
||||
|
||||
Three SwiftData `@Model` types:
|
||||
|
||||
| Model | Purpose | Lifecycle |
|
||||
|-------|---------|-----------|
|
||||
| `GameRecord` | Saved game state (JSON-encoded `GameSnapshot`) | One per variant, deleted on win |
|
||||
| `StatsRecord` | Win/loss/streak per variant+difficulty | Permanent, updated on win/loss |
|
||||
| `PrefsRecord` | Theme, card style, sound toggle | Singleton, updated on change |
|
||||
|
||||
## File Organization
|
||||
|
||||
Files are organized by architectural layer, not by feature:
|
||||
- **Models/** — pure data types, no dependencies
|
||||
- **GameEngine/** — game logic, depends only on Models
|
||||
- **ViewModels/** — UI state management, depends on GameEngine + Services
|
||||
- **Views/** — SwiftUI views, depends on ViewModels
|
||||
- **Services/** — sound, haptics, timer (side effects)
|
||||
- **Persistence/** — SwiftData models + manager
|
||||
- **Theme/** — color definitions and theme state
|
||||
|
||||
This layering ensures that GameEngine can be tested without any UI or persistence dependencies.
|
||||
127
SETUP.md
|
|
@ -1,127 +0,0 @@
|
|||
# SETUP.md — SoliCards
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Xcode 16.3+** (or latest stable)
|
||||
- **macOS 14+** (Sonoma or later)
|
||||
- **xcodegen** (for project generation from `project.yml`)
|
||||
|
||||
Install xcodegen:
|
||||
```bash
|
||||
brew install xcodegen
|
||||
```
|
||||
|
||||
No other dependencies. The project has zero external packages.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Generate the Xcode project
|
||||
|
||||
```bash
|
||||
xcodegen generate
|
||||
```
|
||||
|
||||
This reads `project.yml` and creates `SoliCards.xcodeproj`.
|
||||
|
||||
### 2. Open in Xcode
|
||||
|
||||
```bash
|
||||
open SoliCards.xcodeproj
|
||||
```
|
||||
|
||||
### 3. Select a target and run
|
||||
|
||||
| Target | Destination | Notes |
|
||||
|--------|-------------|-------|
|
||||
| SoliCards | iPhone/iPad Simulator | iOS 17+ |
|
||||
| SoliCards | My Mac | macOS 14+ (native) |
|
||||
| SoliCardsTests | iPhone Simulator | Unit tests |
|
||||
|
||||
## Build Commands (CLI)
|
||||
|
||||
### Debug build (iOS)
|
||||
```bash
|
||||
xcodebuild build \
|
||||
-project SoliCards.xcodeproj \
|
||||
-scheme SoliCards \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16'
|
||||
```
|
||||
|
||||
### Debug build (macOS)
|
||||
```bash
|
||||
xcodebuild build \
|
||||
-project SoliCards.xcodeproj \
|
||||
-scheme SoliCards \
|
||||
-destination 'platform=macOS' \
|
||||
CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO
|
||||
```
|
||||
|
||||
### Release build
|
||||
```bash
|
||||
xcodebuild build \
|
||||
-project SoliCards.xcodeproj \
|
||||
-scheme SoliCards \
|
||||
-configuration Release \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16'
|
||||
```
|
||||
|
||||
### Run tests
|
||||
```bash
|
||||
xcodebuild test \
|
||||
-project SoliCards.xcodeproj \
|
||||
-scheme SoliCardsTests \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16'
|
||||
```
|
||||
|
||||
### Static analysis
|
||||
```bash
|
||||
xcodebuild analyze \
|
||||
-project SoliCards.xcodeproj \
|
||||
-scheme SoliCards \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16'
|
||||
```
|
||||
|
||||
## Schemes
|
||||
|
||||
| Scheme | Purpose |
|
||||
|--------|---------|
|
||||
| SoliCards | Main app (iOS + macOS multiplatform) |
|
||||
| SoliCardsTests | Unit test bundle (57 tests, 12 suites) |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
SoliCards/ # 53 Swift source files
|
||||
├── SoliCardsApp.swift # @main entry point
|
||||
├── ContentView.swift # Root navigation, persistence wiring
|
||||
├── Models/ # Card, Suit, Rank, GameVariant, Difficulty, etc.
|
||||
├── GameEngine/ # GameRules protocol + 3 variant implementations
|
||||
├── ViewModels/ # GameViewModel, SettingsViewModel, StatsViewModel
|
||||
├── Views/ # SwiftUI views (Game, Menu, Settings, Statistics)
|
||||
├── Services/ # SoundManager, TimerService, HapticManager
|
||||
├── Persistence/ # SwiftData models + PersistenceManager
|
||||
├── Theme/ # GameTheme, ThemeManager
|
||||
├── Extensions/ # CardLayout, Array+Card
|
||||
└── Resources/ # Assets.xcassets, Localizable.xcstrings, PrivacyInfo
|
||||
|
||||
SoliCardsTests/ # 9 test files, 57 tests
|
||||
├── Models/ # CardTests, DeckTests, DifficultyTests
|
||||
└── GameEngine/ # KlondikeRulesTests, SpiderRulesTests, FreeCellRulesTests,
|
||||
# MoveValidatorTests, AutoCompleterTests, GameStateTests
|
||||
```
|
||||
|
||||
## Card Assets
|
||||
|
||||
- **52 card front PNGs** imported from `game-SoliCards/svg_playing_cards/fronts/`
|
||||
- **12 card back PNGs** imported from `game-SoliCards/svg_playing_cards/backs/`
|
||||
- All stored in `SoliCards/Resources/Assets.xcassets/` as imagesets
|
||||
|
||||
## Regenerating the Project
|
||||
|
||||
If you modify `project.yml` (add targets, change settings, etc.):
|
||||
|
||||
```bash
|
||||
xcodegen generate
|
||||
```
|
||||
|
||||
The `.xcodeproj` is generated — do not edit it by hand.
|
||||
|
|
@ -1,823 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
05F732433249D0777BFE8F91 /* KlondikeRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31155959181B8D516A82096 /* KlondikeRulesTests.swift */; };
|
||||
0D292BDC30C9D09CF4677FFA /* SpiderBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2065F4D438686C32E01E449F /* SpiderBoardView.swift */; };
|
||||
0DB00631BF90F0626BAE21CD /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60AA0F17A17B13707CB0228 /* SettingsViewModel.swift */; };
|
||||
12A564800C8BAED47DAD0C47 /* GameState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3488A79053BB9BF5BF6C39 /* GameState.swift */; };
|
||||
171B8FB2868AAE8D3198B05E /* CardStylePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF9D48C269BE378EF381ADF6 /* CardStylePickerView.swift */; };
|
||||
19E266494C3F614EEEC6539B /* GameVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31B3F7875D68C0BA0B8FB54 /* GameVariant.swift */; };
|
||||
1A5B631C600469C9F28CF783 /* DraggedCardsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01DD3D6B3B0DEA17B94C81C /* DraggedCardsOverlay.swift */; };
|
||||
29B7A35FE0C531B8CF8BF814 /* GameStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E9415DE47EA69B52839CA5 /* GameStateTests.swift */; };
|
||||
2A123DBA7BA8AB08CADD6BDF /* CardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2E837BB1ADC907A8F991A0 /* CardLayout.swift */; };
|
||||
34CB07FE798181D502428D1D /* Array+Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DAC2213D876C2E0F48CBB3 /* Array+Card.swift */; };
|
||||
37D0960283347728B64A2FB4 /* MoveValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D83697D866ACA495ED4B8 /* MoveValidator.swift */; };
|
||||
3997014B0E7EA0A651AB0216 /* FreeCellRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8A7187F45271D5F9FBB0A07 /* FreeCellRulesTests.swift */; };
|
||||
3D6A93AB14AEAD9E65BB181D /* SpiderRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC022E983E9801D0A3A448B4 /* SpiderRulesTests.swift */; };
|
||||
3E5C08ACC630D771613ECA16 /* MainMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6AE9622A04A543946250427 /* MainMenuView.swift */; };
|
||||
40A295F6F752B57C290C49C5 /* SoliCardsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2123C4E1A2A72E80EC1123A5 /* SoliCardsApp.swift */; };
|
||||
42FC69591B33DD5370784036 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA638FEAAD15EBE1E78FFE14 /* CardView.swift */; };
|
||||
43F316092B0C12D7A7276309 /* CardBackPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB728E0216832C7887E0B0E4 /* CardBackPickerView.swift */; };
|
||||
452209A28A7E2AA36CDFEFCA /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8595FFC9DDE3EA38CE0A617B /* PrivacyInfo.xcprivacy */; };
|
||||
4BF879FC8DB20D9ED32D4738 /* AutoCompleterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A81A01510385FC02760FB4 /* AutoCompleterTests.swift */; };
|
||||
537755217C0912F24EE2F5C6 /* NewGameSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1321C0FD7F644967F371B7A /* NewGameSheet.swift */; };
|
||||
5550A93B56A7BE882A1C7303 /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA573DB9A21FE34ECFF6794 /* PersistenceManager.swift */; };
|
||||
55A796BFBF38FBA8B6C6808F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9810F299E93134997859B2BD /* ContentView.swift */; };
|
||||
56B286A65F7D8845885E9AF1 /* MoveAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E568D8FC36EF5D9937E0F97 /* MoveAction.swift */; };
|
||||
56D2056F3CA46D09F2D94F10 /* StatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634429F416F1A484018E084E /* StatisticsView.swift */; };
|
||||
586EA99FAD450DB90C430CA0 /* GamePhase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 488C04322FF37B821C662516 /* GamePhase.swift */; };
|
||||
5B89DF181690B09ED9204598 /* GameViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B4677EC6C93B738F443D86 /* GameViewModel.swift */; };
|
||||
5D4B0E1123A7870248DA8360 /* AutoCompleter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 115321F0A0475538BA016151 /* AutoCompleter.swift */; };
|
||||
5E2EA0FE1F0216236DF6BE56 /* FreeCellBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE7880E1D80183ABCFBF49F4 /* FreeCellBoardView.swift */; };
|
||||
60111A0FEC2BB3227E4E02E4 /* ScoreBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5C2150140E0A5A3C4CD18D0 /* ScoreBarView.swift */; };
|
||||
64E9B99DFBCD0F1D59F3D890 /* Difficulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = A64A7B696016187523AD7277 /* Difficulty.swift */; };
|
||||
6585CAA4B2AD823C1F6C20DE /* GameTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2BABA5FED1321ABA0A2F55 /* GameTheme.swift */; };
|
||||
65E127C9C91486AF86F9A693 /* SoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD303449EB1172C9B52E624 /* SoundManager.swift */; };
|
||||
65E698A14BC2C0E659A46128 /* MoveValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4901135D3529A476DEF089F /* MoveValidatorTests.swift */; };
|
||||
7084235B866C1493AC18A061 /* Rank.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9B49166D2E8BE0D9AA2B46 /* Rank.swift */; };
|
||||
70C69A048BE67CDD84E38B26 /* StatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D4A7ACB3BD58FB808D7F89 /* StatsViewModel.swift */; };
|
||||
79BCE98C4E8F851A36D4F753 /* KlondikeRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BD39F68FBBFFB35FCB0EC5 /* KlondikeRules.swift */; };
|
||||
7D5D7789D2BA535A59EFE6EA /* CardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7D93BC01A3997DD183D368 /* CardTests.swift */; };
|
||||
800459364BF6FD875D15171D /* DropTargetPreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D311E1FE85DB24EA48404B /* DropTargetPreferenceKey.swift */; };
|
||||
81B92CA2BDA948B74EEDA6B9 /* VictoryOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE1E3886FE8A8FA0DD3C54A /* VictoryOverlayView.swift */; };
|
||||
8E4A7DFD89F451F7499F2684 /* RulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04FAD6B4305ABFB4769DE792 /* RulesView.swift */; };
|
||||
8F37719FF9A39485718BAD78 /* KlondikeBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403990D89B8854B6A45A44A2 /* KlondikeBoardView.swift */; };
|
||||
A4BD7013B7979EA922BA5041 /* CardStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE2B9027264A1134D2B65DC /* CardStackView.swift */; };
|
||||
A5D797208B75A1C7D23AFB64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB81C7AA5EDF842B3D841907 /* Assets.xcassets */; };
|
||||
A68F6A65F5897179FA3632BF /* TimerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A61A63D3A6077A80ED09B24 /* TimerService.swift */; };
|
||||
AA99FA31E0F4094CBE06FFAF /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16BEB4EF07C82D8B2A89D260 /* Deck.swift */; };
|
||||
B7A5F011A7C538B27F382FB5 /* GameSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF99A0055488CAE6E45D4A3 /* GameSnapshot.swift */; };
|
||||
BA010A1D6573E1DA0D182EF8 /* CardLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA7C39CE680C4AFC5D7959BC /* CardLocation.swift */; };
|
||||
BAD817F065BBAAC4B7C89043 /* GameBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E14435814D19CE147D6D408 /* GameBoardView.swift */; };
|
||||
CBEE7677D3E2D9A9CFAA90D0 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B25C883D3B49CADA34775F1 /* SettingsView.swift */; };
|
||||
CFDAB7D49E3D6CBB420F4CF1 /* HintResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 864D497FD148C1DCA5761247 /* HintResult.swift */; };
|
||||
D00C18D126362C368802C9E3 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72E42967DA3AA21AEE42F177 /* Card.swift */; };
|
||||
D24334636360CE9659A3040A /* GameRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4991B4E4B3E6B9AC2EDA629F /* GameRecord.swift */; };
|
||||
D4520E37CA093BDA7DBAB7F5 /* DifficultyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58632F749B766B0E79DD0152 /* DifficultyTests.swift */; };
|
||||
D75EE30A448E01FD3A2EAD0A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 714F0FD173ED0BC8E680E85A /* Localizable.xcstrings */; };
|
||||
D780D65841792E26E9208B8D /* GameRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F1DE1DBC8F3FFC732DA618 /* GameRules.swift */; };
|
||||
D9E61277B5EA0772F7B28BFF /* ThemePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DEF0FCB8B782702D108DF3 /* ThemePickerView.swift */; };
|
||||
DC0D004F619DE9C61C6AF585 /* PrefsRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1391353BDCE5C135A9FA1F0A /* PrefsRecord.swift */; };
|
||||
E15025574906B1F1C9A03C8F /* SpiderRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4319F36333564A75A3FEB0 /* SpiderRules.swift */; };
|
||||
E9ED2A8341168D2915B4F495 /* DeckTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B14B437E3A3475278EC5 /* DeckTests.swift */; };
|
||||
F02DBFB48DB80D36451D436B /* FreeCellRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ABFB6829A8D0B281CCD3E9 /* FreeCellRules.swift */; };
|
||||
F3AA49C17343374CF1B82FF7 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396230FC7779389B46BE1246 /* ThemeManager.swift */; };
|
||||
F4C3C28A20ECA87EC97FD2DC /* Suit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116E45BE9C698645A16CF6F3 /* Suit.swift */; };
|
||||
F713DA5821F2B37B2019CA1D /* StatsRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66617C01DF8B874B51A55295 /* StatsRecord.swift */; };
|
||||
FD1C9241508230FD8E553981 /* GameRulesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A1B8A570F6B4F573E29FCA /* GameRulesFactory.swift */; };
|
||||
FDCB1D9771FAC46DCE25A6A6 /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4ED587E94AC6FAE79FDAE8 /* HapticManager.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
0457F9A25F53E04E37959F48 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 867199988082B6D71FDDE2D8 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 82669FCF0B13F7341360B265;
|
||||
remoteInfo = SoliCards;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
04FAD6B4305ABFB4769DE792 /* RulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulesView.swift; sourceTree = "<group>"; };
|
||||
0A61A63D3A6077A80ED09B24 /* TimerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerService.swift; sourceTree = "<group>"; };
|
||||
0B4ED587E94AC6FAE79FDAE8 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
|
||||
0C4319F36333564A75A3FEB0 /* SpiderRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpiderRules.swift; sourceTree = "<group>"; };
|
||||
115321F0A0475538BA016151 /* AutoCompleter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleter.swift; sourceTree = "<group>"; };
|
||||
116E45BE9C698645A16CF6F3 /* Suit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suit.swift; sourceTree = "<group>"; };
|
||||
1391353BDCE5C135A9FA1F0A /* PrefsRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsRecord.swift; sourceTree = "<group>"; };
|
||||
16BEB4EF07C82D8B2A89D260 /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = "<group>"; };
|
||||
1B25C883D3B49CADA34775F1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
1EE2B9027264A1134D2B65DC /* CardStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardStackView.swift; sourceTree = "<group>"; };
|
||||
2065F4D438686C32E01E449F /* SpiderBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpiderBoardView.swift; sourceTree = "<group>"; };
|
||||
2123C4E1A2A72E80EC1123A5 /* SoliCardsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoliCardsApp.swift; sourceTree = "<group>"; };
|
||||
22BD39F68FBBFFB35FCB0EC5 /* KlondikeRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlondikeRules.swift; sourceTree = "<group>"; };
|
||||
23DEF0FCB8B782702D108DF3 /* ThemePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePickerView.swift; sourceTree = "<group>"; };
|
||||
25D311E1FE85DB24EA48404B /* DropTargetPreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropTargetPreferenceKey.swift; sourceTree = "<group>"; };
|
||||
26A1B8A570F6B4F573E29FCA /* GameRulesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRulesFactory.swift; sourceTree = "<group>"; };
|
||||
26F1DE1DBC8F3FFC732DA618 /* GameRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRules.swift; sourceTree = "<group>"; };
|
||||
2BF99A0055488CAE6E45D4A3 /* GameSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameSnapshot.swift; sourceTree = "<group>"; };
|
||||
2E568D8FC36EF5D9937E0F97 /* MoveAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveAction.swift; sourceTree = "<group>"; };
|
||||
396230FC7779389B46BE1246 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
3DD303449EB1172C9B52E624 /* SoundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundManager.swift; sourceTree = "<group>"; };
|
||||
403990D89B8854B6A45A44A2 /* KlondikeBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlondikeBoardView.swift; sourceTree = "<group>"; };
|
||||
488C04322FF37B821C662516 /* GamePhase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamePhase.swift; sourceTree = "<group>"; };
|
||||
4991B4E4B3E6B9AC2EDA629F /* GameRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRecord.swift; sourceTree = "<group>"; };
|
||||
4B7D93BC01A3997DD183D368 /* CardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardTests.swift; sourceTree = "<group>"; };
|
||||
4E2E837BB1ADC907A8F991A0 /* CardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardLayout.swift; sourceTree = "<group>"; };
|
||||
58632F749B766B0E79DD0152 /* DifficultyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DifficultyTests.swift; sourceTree = "<group>"; };
|
||||
5E14435814D19CE147D6D408 /* GameBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameBoardView.swift; sourceTree = "<group>"; };
|
||||
634429F416F1A484018E084E /* StatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsView.swift; sourceTree = "<group>"; };
|
||||
66617C01DF8B874B51A55295 /* StatsRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsRecord.swift; sourceTree = "<group>"; };
|
||||
6C9B49166D2E8BE0D9AA2B46 /* Rank.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rank.swift; sourceTree = "<group>"; };
|
||||
714F0FD173ED0BC8E680E85A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
72E42967DA3AA21AEE42F177 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = "<group>"; };
|
||||
7561129FE301D2A5E3652648 /* SoliCards.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = SoliCards.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7E57D0CF303B42DD18A317F1 /* SoliCardsTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = SoliCardsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8595FFC9DDE3EA38CE0A617B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
864D497FD148C1DCA5761247 /* HintResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HintResult.swift; sourceTree = "<group>"; };
|
||||
94A81A01510385FC02760FB4 /* AutoCompleterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleterTests.swift; sourceTree = "<group>"; };
|
||||
9810F299E93134997859B2BD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
A64A7B696016187523AD7277 /* Difficulty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Difficulty.swift; sourceTree = "<group>"; };
|
||||
A7DAC2213D876C2E0F48CBB3 /* Array+Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Card.swift"; sourceTree = "<group>"; };
|
||||
A8ABFB6829A8D0B281CCD3E9 /* FreeCellRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeCellRules.swift; sourceTree = "<group>"; };
|
||||
A8E9415DE47EA69B52839CA5 /* GameStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStateTests.swift; sourceTree = "<group>"; };
|
||||
AAA573DB9A21FE34ECFF6794 /* PersistenceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceManager.swift; sourceTree = "<group>"; };
|
||||
AE7880E1D80183ABCFBF49F4 /* FreeCellBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeCellBoardView.swift; sourceTree = "<group>"; };
|
||||
B1321C0FD7F644967F371B7A /* NewGameSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGameSheet.swift; sourceTree = "<group>"; };
|
||||
B31155959181B8D516A82096 /* KlondikeRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlondikeRulesTests.swift; sourceTree = "<group>"; };
|
||||
B8D4A7ACB3BD58FB808D7F89 /* StatsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsViewModel.swift; sourceTree = "<group>"; };
|
||||
BC022E983E9801D0A3A448B4 /* SpiderRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpiderRulesTests.swift; sourceTree = "<group>"; };
|
||||
C01DD3D6B3B0DEA17B94C81C /* DraggedCardsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedCardsOverlay.swift; sourceTree = "<group>"; };
|
||||
C0B4677EC6C93B738F443D86 /* GameViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewModel.swift; sourceTree = "<group>"; };
|
||||
C38D83697D866ACA495ED4B8 /* MoveValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveValidator.swift; sourceTree = "<group>"; };
|
||||
C4901135D3529A476DEF089F /* MoveValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveValidatorTests.swift; sourceTree = "<group>"; };
|
||||
C5C2150140E0A5A3C4CD18D0 /* ScoreBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreBarView.swift; sourceTree = "<group>"; };
|
||||
C60AA0F17A17B13707CB0228 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
C8A7187F45271D5F9FBB0A07 /* FreeCellRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeCellRulesTests.swift; sourceTree = "<group>"; };
|
||||
CA7C39CE680C4AFC5D7959BC /* CardLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardLocation.swift; sourceTree = "<group>"; };
|
||||
CB728E0216832C7887E0B0E4 /* CardBackPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBackPickerView.swift; sourceTree = "<group>"; };
|
||||
CCE1E3886FE8A8FA0DD3C54A /* VictoryOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VictoryOverlayView.swift; sourceTree = "<group>"; };
|
||||
CD2BABA5FED1321ABA0A2F55 /* GameTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameTheme.swift; sourceTree = "<group>"; };
|
||||
CD3488A79053BB9BF5BF6C39 /* GameState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameState.swift; sourceTree = "<group>"; };
|
||||
DA638FEAAD15EBE1E78FFE14 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; };
|
||||
DB81C7AA5EDF842B3D841907 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
E6AE9622A04A543946250427 /* MainMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuView.swift; sourceTree = "<group>"; };
|
||||
EF9D48C269BE378EF381ADF6 /* CardStylePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardStylePickerView.swift; sourceTree = "<group>"; };
|
||||
F31B3F7875D68C0BA0B8FB54 /* GameVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameVariant.swift; sourceTree = "<group>"; };
|
||||
FDF0B14B437E3A3475278EC5 /* DeckTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
08AF7AF8C77BED3EFA6A9AEA /* Statistics */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
634429F416F1A484018E084E /* StatisticsView.swift */,
|
||||
);
|
||||
path = Statistics;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0FF95606A40279B4D3390FBA = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2182AF78CEA93B7431E85608 /* SoliCards */,
|
||||
D4605BF9A1E255A1F2E44B4F /* SoliCardsTests */,
|
||||
9CCB9EC5C71BA2DF24919C93 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2182AF78CEA93B7431E85608 /* SoliCards */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9810F299E93134997859B2BD /* ContentView.swift */,
|
||||
2123C4E1A2A72E80EC1123A5 /* SoliCardsApp.swift */,
|
||||
E4D19DAEAABB74B9FDD84B73 /* Extensions */,
|
||||
3B2FB59B0E86B8F19E698694 /* GameEngine */,
|
||||
FF9DBC6C605DE18ED6DA5E1C /* Models */,
|
||||
2D71FE366319D21065F80CFD /* Persistence */,
|
||||
3271C08BF426FD74787C6217 /* Resources */,
|
||||
A1D36CB5F370E4F3906E319A /* Services */,
|
||||
A8376EC525EE5E88D4517BA1 /* Theme */,
|
||||
C6C973AB5DCD1C0E766F633A /* ViewModels */,
|
||||
F84CF6704CBB01C0C9875CE2 /* Views */,
|
||||
);
|
||||
path = SoliCards;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D71FE366319D21065F80CFD /* Persistence */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4991B4E4B3E6B9AC2EDA629F /* GameRecord.swift */,
|
||||
AAA573DB9A21FE34ECFF6794 /* PersistenceManager.swift */,
|
||||
1391353BDCE5C135A9FA1F0A /* PrefsRecord.swift */,
|
||||
66617C01DF8B874B51A55295 /* StatsRecord.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3271C08BF426FD74787C6217 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB81C7AA5EDF842B3D841907 /* Assets.xcassets */,
|
||||
714F0FD173ED0BC8E680E85A /* Localizable.xcstrings */,
|
||||
8595FFC9DDE3EA38CE0A617B /* PrivacyInfo.xcprivacy */,
|
||||
AB3F10466EA26B5E0B5827A8 /* Sounds */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3B2FB59B0E86B8F19E698694 /* GameEngine */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
115321F0A0475538BA016151 /* AutoCompleter.swift */,
|
||||
A8ABFB6829A8D0B281CCD3E9 /* FreeCellRules.swift */,
|
||||
26F1DE1DBC8F3FFC732DA618 /* GameRules.swift */,
|
||||
26A1B8A570F6B4F573E29FCA /* GameRulesFactory.swift */,
|
||||
CD3488A79053BB9BF5BF6C39 /* GameState.swift */,
|
||||
22BD39F68FBBFFB35FCB0EC5 /* KlondikeRules.swift */,
|
||||
C38D83697D866ACA495ED4B8 /* MoveValidator.swift */,
|
||||
0C4319F36333564A75A3FEB0 /* SpiderRules.swift */,
|
||||
);
|
||||
path = GameEngine;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
45A518EB42C0CDB893716BBE /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CB728E0216832C7887E0B0E4 /* CardBackPickerView.swift */,
|
||||
EF9D48C269BE378EF381ADF6 /* CardStylePickerView.swift */,
|
||||
1B25C883D3B49CADA34775F1 /* SettingsView.swift */,
|
||||
23DEF0FCB8B782702D108DF3 /* ThemePickerView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7A41E8DA0C1CCBC6C102E3A0 /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9CCB9EC5C71BA2DF24919C93 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7561129FE301D2A5E3652648 /* SoliCards.app */,
|
||||
7E57D0CF303B42DD18A317F1 /* SoliCardsTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1D36CB5F370E4F3906E319A /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0B4ED587E94AC6FAE79FDAE8 /* HapticManager.swift */,
|
||||
3DD303449EB1172C9B52E624 /* SoundManager.swift */,
|
||||
0A61A63D3A6077A80ED09B24 /* TimerService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5353C257EB10EB9526C6E39 /* Menu */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E6AE9622A04A543946250427 /* MainMenuView.swift */,
|
||||
B1321C0FD7F644967F371B7A /* NewGameSheet.swift */,
|
||||
04FAD6B4305ABFB4769DE792 /* RulesView.swift */,
|
||||
);
|
||||
path = Menu;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A8376EC525EE5E88D4517BA1 /* Theme */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CD2BABA5FED1321ABA0A2F55 /* GameTheme.swift */,
|
||||
396230FC7779389B46BE1246 /* ThemeManager.swift */,
|
||||
);
|
||||
path = Theme;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AB3F10466EA26B5E0B5827A8 /* Sounds */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Sounds;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C6C973AB5DCD1C0E766F633A /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C0B4677EC6C93B738F443D86 /* GameViewModel.swift */,
|
||||
C60AA0F17A17B13707CB0228 /* SettingsViewModel.swift */,
|
||||
B8D4A7ACB3BD58FB808D7F89 /* StatsViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D4605BF9A1E255A1F2E44B4F /* SoliCardsTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB86ADC0F0581A7B56EBE28E /* GameEngine */,
|
||||
E5E67911DEBB2793B3D8F1AD /* Models */,
|
||||
7A41E8DA0C1CCBC6C102E3A0 /* ViewModels */,
|
||||
);
|
||||
path = SoliCardsTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DA0714078CFC7F8BCD993F8C /* Game */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1EE2B9027264A1134D2B65DC /* CardStackView.swift */,
|
||||
DA638FEAAD15EBE1E78FFE14 /* CardView.swift */,
|
||||
C01DD3D6B3B0DEA17B94C81C /* DraggedCardsOverlay.swift */,
|
||||
25D311E1FE85DB24EA48404B /* DropTargetPreferenceKey.swift */,
|
||||
AE7880E1D80183ABCFBF49F4 /* FreeCellBoardView.swift */,
|
||||
5E14435814D19CE147D6D408 /* GameBoardView.swift */,
|
||||
403990D89B8854B6A45A44A2 /* KlondikeBoardView.swift */,
|
||||
C5C2150140E0A5A3C4CD18D0 /* ScoreBarView.swift */,
|
||||
2065F4D438686C32E01E449F /* SpiderBoardView.swift */,
|
||||
CCE1E3886FE8A8FA0DD3C54A /* VictoryOverlayView.swift */,
|
||||
);
|
||||
path = Game;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB86ADC0F0581A7B56EBE28E /* GameEngine */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
94A81A01510385FC02760FB4 /* AutoCompleterTests.swift */,
|
||||
C8A7187F45271D5F9FBB0A07 /* FreeCellRulesTests.swift */,
|
||||
A8E9415DE47EA69B52839CA5 /* GameStateTests.swift */,
|
||||
B31155959181B8D516A82096 /* KlondikeRulesTests.swift */,
|
||||
C4901135D3529A476DEF089F /* MoveValidatorTests.swift */,
|
||||
BC022E983E9801D0A3A448B4 /* SpiderRulesTests.swift */,
|
||||
);
|
||||
path = GameEngine;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E4D19DAEAABB74B9FDD84B73 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A7DAC2213D876C2E0F48CBB3 /* Array+Card.swift */,
|
||||
4E2E837BB1ADC907A8F991A0 /* CardLayout.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E5E67911DEBB2793B3D8F1AD /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B7D93BC01A3997DD183D368 /* CardTests.swift */,
|
||||
FDF0B14B437E3A3475278EC5 /* DeckTests.swift */,
|
||||
58632F749B766B0E79DD0152 /* DifficultyTests.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F84CF6704CBB01C0C9875CE2 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DA0714078CFC7F8BCD993F8C /* Game */,
|
||||
A5353C257EB10EB9526C6E39 /* Menu */,
|
||||
45A518EB42C0CDB893716BBE /* Settings */,
|
||||
08AF7AF8C77BED3EFA6A9AEA /* Statistics */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FF9DBC6C605DE18ED6DA5E1C /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
72E42967DA3AA21AEE42F177 /* Card.swift */,
|
||||
CA7C39CE680C4AFC5D7959BC /* CardLocation.swift */,
|
||||
16BEB4EF07C82D8B2A89D260 /* Deck.swift */,
|
||||
A64A7B696016187523AD7277 /* Difficulty.swift */,
|
||||
488C04322FF37B821C662516 /* GamePhase.swift */,
|
||||
2BF99A0055488CAE6E45D4A3 /* GameSnapshot.swift */,
|
||||
F31B3F7875D68C0BA0B8FB54 /* GameVariant.swift */,
|
||||
864D497FD148C1DCA5761247 /* HintResult.swift */,
|
||||
2E568D8FC36EF5D9937E0F97 /* MoveAction.swift */,
|
||||
6C9B49166D2E8BE0D9AA2B46 /* Rank.swift */,
|
||||
116E45BE9C698645A16CF6F3 /* Suit.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
82669FCF0B13F7341360B265 /* SoliCards */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F6C31F00417F79A014C90960 /* Build configuration list for PBXNativeTarget "SoliCards" */;
|
||||
buildPhases = (
|
||||
8C4FBA4AEB4F83B6D76C3868 /* Sources */,
|
||||
00D9BFDDDE0AF0067CA90858 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SoliCards;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = SoliCards;
|
||||
productReference = 7561129FE301D2A5E3652648 /* SoliCards.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
C27736B4220A23D60C765E3E /* SoliCardsTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6F270CC209153B7149368FF8 /* Build configuration list for PBXNativeTarget "SoliCardsTests" */;
|
||||
buildPhases = (
|
||||
00839BE9C0947FF6620A82FC /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
CE28FBA2CF52896AC36A1F13 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SoliCardsTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = SoliCardsTests;
|
||||
productReference = 7E57D0CF303B42DD18A317F1 /* SoliCardsTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
867199988082B6D71FDDE2D8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1630;
|
||||
TargetAttributes = {
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 6AE5403E93959B4E9E707F1D /* Build configuration list for PBXProject "SoliCards" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 0FF95606A40279B4D3390FBA;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 9CCB9EC5C71BA2DF24919C93 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
82669FCF0B13F7341360B265 /* SoliCards */,
|
||||
C27736B4220A23D60C765E3E /* SoliCardsTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
00D9BFDDDE0AF0067CA90858 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5D797208B75A1C7D23AFB64 /* Assets.xcassets in Resources */,
|
||||
D75EE30A448E01FD3A2EAD0A /* Localizable.xcstrings in Resources */,
|
||||
452209A28A7E2AA36CDFEFCA /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
00839BE9C0947FF6620A82FC /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4BF879FC8DB20D9ED32D4738 /* AutoCompleterTests.swift in Sources */,
|
||||
7D5D7789D2BA535A59EFE6EA /* CardTests.swift in Sources */,
|
||||
E9ED2A8341168D2915B4F495 /* DeckTests.swift in Sources */,
|
||||
D4520E37CA093BDA7DBAB7F5 /* DifficultyTests.swift in Sources */,
|
||||
3997014B0E7EA0A651AB0216 /* FreeCellRulesTests.swift in Sources */,
|
||||
29B7A35FE0C531B8CF8BF814 /* GameStateTests.swift in Sources */,
|
||||
05F732433249D0777BFE8F91 /* KlondikeRulesTests.swift in Sources */,
|
||||
65E698A14BC2C0E659A46128 /* MoveValidatorTests.swift in Sources */,
|
||||
3D6A93AB14AEAD9E65BB181D /* SpiderRulesTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
8C4FBA4AEB4F83B6D76C3868 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
34CB07FE798181D502428D1D /* Array+Card.swift in Sources */,
|
||||
5D4B0E1123A7870248DA8360 /* AutoCompleter.swift in Sources */,
|
||||
D00C18D126362C368802C9E3 /* Card.swift in Sources */,
|
||||
43F316092B0C12D7A7276309 /* CardBackPickerView.swift in Sources */,
|
||||
2A123DBA7BA8AB08CADD6BDF /* CardLayout.swift in Sources */,
|
||||
BA010A1D6573E1DA0D182EF8 /* CardLocation.swift in Sources */,
|
||||
A4BD7013B7979EA922BA5041 /* CardStackView.swift in Sources */,
|
||||
171B8FB2868AAE8D3198B05E /* CardStylePickerView.swift in Sources */,
|
||||
42FC69591B33DD5370784036 /* CardView.swift in Sources */,
|
||||
55A796BFBF38FBA8B6C6808F /* ContentView.swift in Sources */,
|
||||
AA99FA31E0F4094CBE06FFAF /* Deck.swift in Sources */,
|
||||
64E9B99DFBCD0F1D59F3D890 /* Difficulty.swift in Sources */,
|
||||
1A5B631C600469C9F28CF783 /* DraggedCardsOverlay.swift in Sources */,
|
||||
800459364BF6FD875D15171D /* DropTargetPreferenceKey.swift in Sources */,
|
||||
5E2EA0FE1F0216236DF6BE56 /* FreeCellBoardView.swift in Sources */,
|
||||
F02DBFB48DB80D36451D436B /* FreeCellRules.swift in Sources */,
|
||||
BAD817F065BBAAC4B7C89043 /* GameBoardView.swift in Sources */,
|
||||
586EA99FAD450DB90C430CA0 /* GamePhase.swift in Sources */,
|
||||
D24334636360CE9659A3040A /* GameRecord.swift in Sources */,
|
||||
D780D65841792E26E9208B8D /* GameRules.swift in Sources */,
|
||||
FD1C9241508230FD8E553981 /* GameRulesFactory.swift in Sources */,
|
||||
B7A5F011A7C538B27F382FB5 /* GameSnapshot.swift in Sources */,
|
||||
12A564800C8BAED47DAD0C47 /* GameState.swift in Sources */,
|
||||
6585CAA4B2AD823C1F6C20DE /* GameTheme.swift in Sources */,
|
||||
19E266494C3F614EEEC6539B /* GameVariant.swift in Sources */,
|
||||
5B89DF181690B09ED9204598 /* GameViewModel.swift in Sources */,
|
||||
FDCB1D9771FAC46DCE25A6A6 /* HapticManager.swift in Sources */,
|
||||
CFDAB7D49E3D6CBB420F4CF1 /* HintResult.swift in Sources */,
|
||||
8F37719FF9A39485718BAD78 /* KlondikeBoardView.swift in Sources */,
|
||||
79BCE98C4E8F851A36D4F753 /* KlondikeRules.swift in Sources */,
|
||||
3E5C08ACC630D771613ECA16 /* MainMenuView.swift in Sources */,
|
||||
56B286A65F7D8845885E9AF1 /* MoveAction.swift in Sources */,
|
||||
37D0960283347728B64A2FB4 /* MoveValidator.swift in Sources */,
|
||||
537755217C0912F24EE2F5C6 /* NewGameSheet.swift in Sources */,
|
||||
5550A93B56A7BE882A1C7303 /* PersistenceManager.swift in Sources */,
|
||||
DC0D004F619DE9C61C6AF585 /* PrefsRecord.swift in Sources */,
|
||||
7084235B866C1493AC18A061 /* Rank.swift in Sources */,
|
||||
8E4A7DFD89F451F7499F2684 /* RulesView.swift in Sources */,
|
||||
60111A0FEC2BB3227E4E02E4 /* ScoreBarView.swift in Sources */,
|
||||
CBEE7677D3E2D9A9CFAA90D0 /* SettingsView.swift in Sources */,
|
||||
0DB00631BF90F0626BAE21CD /* SettingsViewModel.swift in Sources */,
|
||||
40A295F6F752B57C290C49C5 /* SoliCardsApp.swift in Sources */,
|
||||
65E127C9C91486AF86F9A693 /* SoundManager.swift in Sources */,
|
||||
0D292BDC30C9D09CF4677FFA /* SpiderBoardView.swift in Sources */,
|
||||
E15025574906B1F1C9A03C8F /* SpiderRules.swift in Sources */,
|
||||
56D2056F3CA46D09F2D94F10 /* StatisticsView.swift in Sources */,
|
||||
F713DA5821F2B37B2019CA1D /* StatsRecord.swift in Sources */,
|
||||
70C69A048BE67CDD84E38B26 /* StatsViewModel.swift in Sources */,
|
||||
F4C3C28A20ECA87EC97FD2DC /* Suit.swift in Sources */,
|
||||
F3AA49C17343374CF1B82FF7 /* ThemeManager.swift in Sources */,
|
||||
D9E61277B5EA0772F7B28BFF /* ThemePickerView.swift in Sources */,
|
||||
A68F6A65F5897179FA3632BF /* TimerService.swift in Sources */,
|
||||
81B92CA2BDA948B74EEDA6B9 /* VictoryOverlayView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
CE28FBA2CF52896AC36A1F13 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 82669FCF0B13F7341360B265 /* SoliCards */;
|
||||
targetProxy = 0457F9A25F53E04E37959F48 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
09C2A01D89409D6B798959DA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SoliCards;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.card-games";
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.solicards.app;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3703194FF4D985541587656D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.solicards.tests;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SoliCards.app/SoliCards";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
57E91C868A14077EF84052C1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.solicards.tests;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SoliCards.app/SoliCards";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
6964E173671E2805B5C3C50E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C48643C0D86B6F2505BC1E97 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SoliCards;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.card-games";
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.solicards.app;
|
||||
SDKROOT = auto;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
DCF01AC30957B9AA76361647 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
6AE5403E93959B4E9E707F1D /* Build configuration list for PBXProject "SoliCards" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
6964E173671E2805B5C3C50E /* Debug */,
|
||||
DCF01AC30957B9AA76361647 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
6F270CC209153B7149368FF8 /* Build configuration list for PBXNativeTarget "SoliCardsTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
57E91C868A14077EF84052C1 /* Debug */,
|
||||
3703194FF4D985541587656D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
F6C31F00417F79A014C90960 /* Build configuration list for PBXNativeTarget "SoliCards" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
09C2A01D89409D6B798959DA /* Debug */,
|
||||
C48643C0D86B6F2505BC1E97 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 867199988082B6D71FDDE2D8 /* Project object */;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@State private var viewModel = GameViewModel()
|
||||
@State private var themeManager = ThemeManager()
|
||||
@State private var cardFaceStyle: CardFaceStyle = .classic
|
||||
@State private var cardBackDesign: CardBackDesign = .blue
|
||||
@State private var showingRules = false
|
||||
@State private var showingNewGame = false
|
||||
@State private var showingSettings = false
|
||||
@State private var showingStats = false
|
||||
@State private var hasLoaded = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
GameBoardView(
|
||||
viewModel: viewModel,
|
||||
theme: themeManager.currentTheme,
|
||||
cardFaceStyle: cardFaceStyle,
|
||||
cardBackDesign: cardBackDesign
|
||||
)
|
||||
.toolbar {
|
||||
#if os(iOS)
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
iOSToolbar
|
||||
}
|
||||
#else
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
macToolbar
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.navigationTitle(viewModel.variant.displayName)
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.visible, for: .bottomBar)
|
||||
.toolbarBackground(.ultraThinMaterial, for: .bottomBar)
|
||||
.toolbarColorScheme(.dark, for: .bottomBar)
|
||||
#endif
|
||||
.tint(.white)
|
||||
.sheet(isPresented: $showingRules) {
|
||||
RulesView(variant: viewModel.variant)
|
||||
}
|
||||
.sheet(isPresented: $showingNewGame) {
|
||||
NewGameSheet(
|
||||
variant: viewModel.variant,
|
||||
difficulty: viewModel.difficulty
|
||||
) { newVariant, newDifficulty in
|
||||
showingNewGame = false
|
||||
viewModel.difficulty = newDifficulty
|
||||
viewModel.changeVariant(to: newVariant)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingSettings) {
|
||||
SettingsView(
|
||||
theme: Binding(
|
||||
get: { themeManager.currentTheme },
|
||||
set: { themeManager.applyTheme($0) }
|
||||
),
|
||||
cardFaceStyle: $cardFaceStyle,
|
||||
cardBackDesign: $cardBackDesign,
|
||||
soundEnabled: Binding(
|
||||
get: { viewModel.isSoundEnabled },
|
||||
set: { viewModel.isSoundEnabled = $0 }
|
||||
)
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showingStats) {
|
||||
StatisticsView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
guard !hasLoaded else { return }
|
||||
hasLoaded = true
|
||||
|
||||
let pm = PersistenceManager(modelContext: modelContext)
|
||||
viewModel.persistenceManager = pm
|
||||
|
||||
let prefs = pm.loadPreferences()
|
||||
if let theme = GameTheme.allThemes.first(where: { $0.id == prefs.themeId }) {
|
||||
themeManager.applyTheme(theme)
|
||||
}
|
||||
if let style = CardFaceStyle(rawValue: prefs.cardFaceStyle) {
|
||||
cardFaceStyle = style
|
||||
}
|
||||
if let back = CardBackDesign(rawValue: prefs.cardBackDesign) {
|
||||
cardBackDesign = back
|
||||
}
|
||||
viewModel.isSoundEnabled = prefs.soundEnabled
|
||||
|
||||
if let savedGame = pm.loadMostRecentGame() {
|
||||
viewModel.resumeGame(from: savedGame)
|
||||
} else {
|
||||
if let savedVariant = GameVariant(rawValue: prefs.lastVariant) {
|
||||
viewModel.changeVariant(to: savedVariant)
|
||||
} else {
|
||||
viewModel.newGame()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: cardFaceStyle) { savePreferences() }
|
||||
.onChange(of: cardBackDesign) { savePreferences() }
|
||||
.onChange(of: themeManager.currentTheme) { savePreferences() }
|
||||
.onChange(of: viewModel.isSoundEnabled) { savePreferences() }
|
||||
#if os(iOS)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
|
||||
viewModel.saveGameNow()
|
||||
savePreferences()
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 800, minHeight: 600)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func savePreferences() {
|
||||
viewModel.persistenceManager?.savePreferences(
|
||||
themeId: themeManager.currentTheme.id,
|
||||
cardFaceStyle: cardFaceStyle,
|
||||
cardBackDesign: cardBackDesign,
|
||||
soundEnabled: viewModel.isSoundEnabled,
|
||||
lastVariant: viewModel.variant,
|
||||
lastDifficulty: viewModel.difficulty
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - iOS Toolbar (bottom bar — 5 items max, with labels)
|
||||
|
||||
@ViewBuilder
|
||||
private var iOSToolbar: some View {
|
||||
Button { showingNewGame = true } label: {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title3)
|
||||
Text("New")
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
.keyboardShortcut("n", modifiers: .command)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button { viewModel.undo() } label: {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: "arrow.uturn.backward.circle.fill")
|
||||
.font(.title3)
|
||||
Text("Undo")
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
.keyboardShortcut("z", modifiers: .command)
|
||||
.disabled(!viewModel.canUndo)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button { viewModel.requestHint() } label: {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: "lightbulb.fill")
|
||||
.font(.title3)
|
||||
Text("Hint")
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
.keyboardShortcut("h", modifiers: [])
|
||||
|
||||
Spacer()
|
||||
|
||||
// "More" menu — consolidates secondary actions
|
||||
Menu {
|
||||
// Game variants
|
||||
Menu("Game Variant") {
|
||||
ForEach(GameVariant.allCases) { variant in
|
||||
Button {
|
||||
viewModel.changeVariant(to: variant)
|
||||
} label: {
|
||||
HStack {
|
||||
Text(variant.displayName)
|
||||
if variant == viewModel.variant {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Difficulty
|
||||
Menu("Difficulty") {
|
||||
ForEach(Difficulty.allCases) { diff in
|
||||
Button {
|
||||
viewModel.difficulty = diff
|
||||
viewModel.newGame()
|
||||
} label: {
|
||||
HStack {
|
||||
Text(diff.displayName)
|
||||
if diff == viewModel.difficulty {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button { viewModel.autoComplete() } label: {
|
||||
Label("Auto-Complete", systemImage: "wand.and.stars")
|
||||
}
|
||||
|
||||
Button {
|
||||
viewModel.isSoundEnabled.toggle()
|
||||
} label: {
|
||||
Label(viewModel.isSoundEnabled ? "Sound On" : "Sound Off",
|
||||
systemImage: viewModel.isSoundEnabled ? "speaker.wave.2" : "speaker.slash")
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button { showingRules = true } label: {
|
||||
Label("Rules", systemImage: "book")
|
||||
}
|
||||
|
||||
Button { showingStats = true } label: {
|
||||
Label("Statistics", systemImage: "chart.bar")
|
||||
}
|
||||
|
||||
Button { showingSettings = true } label: {
|
||||
Label("Settings", systemImage: "gearshape")
|
||||
}
|
||||
} label: {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: "ellipsis.circle.fill")
|
||||
.font(.title3)
|
||||
Text("More")
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - macOS Toolbar (all items fit, no labels needed)
|
||||
|
||||
@ViewBuilder
|
||||
private var macToolbar: some View {
|
||||
Menu {
|
||||
ForEach(GameVariant.allCases) { variant in
|
||||
Button(variant.displayName) {
|
||||
viewModel.changeVariant(to: variant)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
ForEach(Difficulty.allCases) { diff in
|
||||
Button {
|
||||
viewModel.difficulty = diff
|
||||
viewModel.newGame()
|
||||
} label: {
|
||||
HStack {
|
||||
Text(diff.displayName)
|
||||
if diff == viewModel.difficulty {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Game", systemImage: "suit.spade.fill")
|
||||
}
|
||||
|
||||
Button { showingNewGame = true } label: {
|
||||
Label("New Game", systemImage: "plus.circle")
|
||||
}
|
||||
.keyboardShortcut("n", modifiers: .command)
|
||||
|
||||
Button { viewModel.undo() } label: {
|
||||
Label("Undo", systemImage: "arrow.uturn.backward")
|
||||
}
|
||||
.keyboardShortcut("z", modifiers: .command)
|
||||
.disabled(!viewModel.canUndo)
|
||||
|
||||
Button { viewModel.requestHint() } label: {
|
||||
Label("Hint", systemImage: "lightbulb")
|
||||
}
|
||||
.keyboardShortcut("h", modifiers: [])
|
||||
|
||||
Button { viewModel.autoComplete() } label: {
|
||||
Label("Auto", systemImage: "wand.and.stars")
|
||||
}
|
||||
|
||||
Button { showingRules = true } label: {
|
||||
Label("Rules", systemImage: "book")
|
||||
}
|
||||
|
||||
Button { showingStats = true } label: {
|
||||
Label("Stats", systemImage: "chart.bar")
|
||||
}
|
||||
|
||||
Button { showingSettings = true } label: {
|
||||
Label("Settings", systemImage: "gearshape")
|
||||
}
|
||||
|
||||
Button {
|
||||
viewModel.isSoundEnabled.toggle()
|
||||
} label: {
|
||||
Label("Sound", systemImage: viewModel.isSoundEnabled ? "speaker.wave.2" : "speaker.slash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
extension Array where Element == Card {
|
||||
/// Returns the suffix of face-up cards from the end of the array.
|
||||
var faceUpSuffix: [Card] {
|
||||
var result: [Card] = []
|
||||
for card in reversed() {
|
||||
guard card.isFaceUp else { break }
|
||||
result.insert(card, at: 0)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Returns the top card (last element), or nil if empty.
|
||||
var topCard: Card? { last }
|
||||
}
|
||||
|
||||
extension Array {
|
||||
/// Returns the longest suffix where all elements satisfy the predicate.
|
||||
func trailingSuffix(while predicate: (Element) -> Bool) -> ArraySlice<Element> {
|
||||
var startIndex = endIndex
|
||||
for index in indices.reversed() {
|
||||
guard predicate(self[index]) else { break }
|
||||
startIndex = index
|
||||
}
|
||||
return self[startIndex..<endIndex]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
struct CardLayout {
|
||||
let availableSize: CGSize
|
||||
let variant: GameVariant
|
||||
let deepestColumn: (faceDown: Int, faceUp: Int)
|
||||
|
||||
var columnCount: Int { variant.tableauCount }
|
||||
var isLandscape: Bool { availableSize.width > availableSize.height }
|
||||
|
||||
/// Card size — constrained by the tighter of width or height.
|
||||
var cardWidth: CGFloat {
|
||||
let computed = min(widthConstrainedCardWidth, heightConstrainedCardWidth)
|
||||
#if os(macOS)
|
||||
// Cap card width on macOS so cards don't get absurdly large on big displays
|
||||
return min(computed, 120)
|
||||
#else
|
||||
return computed
|
||||
#endif
|
||||
}
|
||||
|
||||
var cardHeight: CGFloat { cardWidth * 1.4 }
|
||||
|
||||
/// Padding between cards. In landscape, this expands to fill the available width
|
||||
/// so cards spread evenly across the screen.
|
||||
var horizontalPadding: CGFloat {
|
||||
let basePadding = max(2, availableSize.width * 0.008)
|
||||
|
||||
// If height-constrained (landscape), distribute extra width as padding
|
||||
if heightConstrainedCardWidth < widthConstrainedCardWidth {
|
||||
let totalCardWidth = CGFloat(columnCount) * cardWidth
|
||||
let availableForPadding = availableSize.width - totalCardWidth
|
||||
let gaps = CGFloat(columnCount + 1)
|
||||
return max(basePadding, availableForPadding / gaps)
|
||||
}
|
||||
|
||||
return basePadding
|
||||
}
|
||||
|
||||
var verticalOverlapFaceDown: CGFloat { cardHeight * 0.15 }
|
||||
var verticalOverlapFaceUp: CGFloat { cardHeight * 0.25 }
|
||||
var cornerRadius: CGFloat { cardWidth * 0.08 }
|
||||
|
||||
func cardSize() -> CGSize {
|
||||
CGSize(width: cardWidth, height: cardHeight)
|
||||
}
|
||||
|
||||
var touchTargetPadding: CGFloat {
|
||||
max(0, (44 - cardWidth) / 2)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var basePadding: CGFloat {
|
||||
max(2, availableSize.width * 0.008)
|
||||
}
|
||||
|
||||
private var widthConstrainedCardWidth: CGFloat {
|
||||
let totalPadding = basePadding * CGFloat(columnCount + 1)
|
||||
return (availableSize.width - totalPadding) / CGFloat(columnCount)
|
||||
}
|
||||
|
||||
private var heightConstrainedCardWidth: CGFloat {
|
||||
let d = CGFloat(deepestColumn.faceDown)
|
||||
let u = CGFloat(max(0, deepestColumn.faceUp - 1))
|
||||
let totalFactor = 1.0 + 0.1 + (d * 0.15 + u * 0.25 + 1.0)
|
||||
|
||||
// Use 92% of available height to leave room for VStack spacing,
|
||||
// bottom padding, and any unmeasured platform chrome
|
||||
let usableHeight = availableSize.height * 0.92
|
||||
let base = max(30, usableHeight / (1.4 * totalFactor))
|
||||
|
||||
#if os(iOS)
|
||||
return isLandscape ? base * 1.3 : base
|
||||
#else
|
||||
return base
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum AutoCompleter {
|
||||
/// Finds the next card that can be auto-moved to a foundation.
|
||||
/// Returns the source location and foundation index, or nil if no move is available.
|
||||
static func findNextAutoMove(state: GameSnapshot, rules: GameRules) -> (from: CardLocation, to: CardLocation)? {
|
||||
// Check waste pile
|
||||
if let topWaste = state.waste.last {
|
||||
for (i, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(topWaste, topCard: foundation.last) {
|
||||
return (.waste, .foundation(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check free cells
|
||||
for (cellIndex, cell) in state.freeCells.enumerated() {
|
||||
guard let card = cell else { continue }
|
||||
for (i, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(card, topCard: foundation.last) {
|
||||
return (.freeCell(cellIndex), .foundation(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check tableau tops
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
guard let topCard = tableau.last, topCard.isFaceUp else { continue }
|
||||
for (i, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(topCard, topCard: foundation.last) {
|
||||
return (.tableau(tabIndex), .foundation(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct FreeCellRules: GameRules {
|
||||
let variant: GameVariant = .freeCell
|
||||
|
||||
func deal(deck: [Card]) -> GameSnapshot {
|
||||
var remaining = deck
|
||||
var tableaus: [[Card]] = []
|
||||
|
||||
// Deal all 52 cards to 8 tableaus, all face-up
|
||||
// First 4 tableaus get 7 cards, last 4 get 6 cards
|
||||
for i in 0..<8 {
|
||||
let cardCount = i < 4 ? 7 : 6
|
||||
var column: [Card] = []
|
||||
for _ in 0..<cardCount {
|
||||
var card = remaining.removeFirst()
|
||||
card.isFaceUp = true
|
||||
column.append(card)
|
||||
}
|
||||
tableaus.append(column)
|
||||
}
|
||||
|
||||
return GameSnapshot(
|
||||
tableaus: tableaus,
|
||||
foundations: [[], [], [], []],
|
||||
stock: [],
|
||||
waste: [],
|
||||
freeCells: [nil, nil, nil, nil],
|
||||
moves: 0,
|
||||
score: 0
|
||||
)
|
||||
}
|
||||
|
||||
func canMove(cards: [Card], from: CardLocation, to: CardLocation, state: GameSnapshot) -> Bool {
|
||||
guard let firstCard = cards.first else { return false }
|
||||
|
||||
switch to {
|
||||
case .foundation(let index):
|
||||
guard cards.count == 1 else { return false }
|
||||
guard index >= 0, index < state.foundations.count else { return false }
|
||||
return MoveValidator.canPlaceOnFoundation(firstCard, topCard: state.foundations[index].last)
|
||||
|
||||
case .tableau(let index):
|
||||
guard index >= 0, index < state.tableaus.count else { return false }
|
||||
let maxMovable = calculateMaxMovableCards(state: state, targetEmpty: state.tableaus[index].isEmpty)
|
||||
guard cards.count <= maxMovable else { return false }
|
||||
|
||||
let tableau = state.tableaus[index]
|
||||
if tableau.isEmpty { return true }
|
||||
guard let topCard = tableau.last else { return false }
|
||||
return MoveValidator.isAlternatingColor(firstCard, with: topCard)
|
||||
&& MoveValidator.isDescending(firstCard, onto: topCard)
|
||||
|
||||
case .freeCell(let index):
|
||||
guard cards.count == 1 else { return false }
|
||||
guard index >= 0, index < state.freeCells.count else { return false }
|
||||
return state.freeCells[index] == nil
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func drawFromStock(state: inout GameSnapshot, drawCount: Int) -> MoveAction? {
|
||||
nil // FreeCell has no stock
|
||||
}
|
||||
|
||||
func scoreForMove(from: CardLocation, to: CardLocation) -> Int {
|
||||
switch (from, to) {
|
||||
case (_, .foundation): return 10
|
||||
case (.freeCell, .tableau): return 5
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
func isWon(state: GameSnapshot) -> Bool {
|
||||
state.foundations.allSatisfy { $0.count == 13 }
|
||||
}
|
||||
|
||||
func canAutoComplete(state: GameSnapshot) -> Bool {
|
||||
// All tableau cards must be face-up and in descending order
|
||||
let allFaceUp = state.tableaus.allSatisfy { $0.allSatisfy { $0.isFaceUp } }
|
||||
guard allFaceUp else { return false }
|
||||
|
||||
// Check that all tableaus are in valid descending alternating-color order
|
||||
for tableau in state.tableaus {
|
||||
for i in 1..<tableau.count {
|
||||
guard MoveValidator.isAlternatingColor(tableau[i], with: tableau[i - 1]),
|
||||
MoveValidator.isDescending(tableau[i], onto: tableau[i - 1]) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/// Power move formula: (1 + empty_freecells) × 2^empty_tableaus
|
||||
func calculateMaxMovableCards(state: GameSnapshot, targetEmpty: Bool) -> Int {
|
||||
let emptyFreeCells = state.freeCells.filter { $0 == nil }.count
|
||||
// Don't count the target tableau as empty if we're moving to an empty tableau
|
||||
let emptyTableaus = state.tableaus.filter { $0.isEmpty }.count - (targetEmpty ? 1 : 0)
|
||||
let adjustedEmptyTableaus = max(0, emptyTableaus)
|
||||
return (1 + emptyFreeCells) * Int(pow(2.0, Double(adjustedEmptyTableaus)))
|
||||
}
|
||||
|
||||
func findHints(state: GameSnapshot, settings: DifficultySettings) -> [HintResult] {
|
||||
guard settings.hintsEnabled else { return [] }
|
||||
var hints: [HintResult] = []
|
||||
|
||||
// Priority 1: Aces to foundation (from tableaus and free cells)
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
guard let topCard = tableau.last, MoveValidator.isAce(topCard) else { continue }
|
||||
for (fIndex, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(topCard, topCard: foundation.last) {
|
||||
hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .foundation(fIndex), priority: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (cellIndex, cell) in state.freeCells.enumerated() {
|
||||
guard let card = cell, MoveValidator.isAce(card) else { continue }
|
||||
for (fIndex, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(card, topCard: foundation.last) {
|
||||
hints.append(HintResult(cards: [card], from: .freeCell(cellIndex), to: .foundation(fIndex), priority: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Other foundation moves
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
guard let topCard = tableau.last, !MoveValidator.isAce(topCard) else { continue }
|
||||
for (fIndex, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(topCard, topCard: foundation.last) {
|
||||
hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .foundation(fIndex), priority: 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (cellIndex, cell) in state.freeCells.enumerated() {
|
||||
guard let card = cell, !MoveValidator.isAce(card) else { continue }
|
||||
for (fIndex, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(card, topCard: foundation.last) {
|
||||
hints.append(HintResult(cards: [card], from: .freeCell(cellIndex), to: .foundation(fIndex), priority: 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Tableau to tableau moves
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
guard let topCard = tableau.last else { continue }
|
||||
for (destIndex, _) in state.tableaus.enumerated() {
|
||||
guard destIndex != tabIndex else { continue }
|
||||
if canMove(cards: [topCard], from: .tableau(tabIndex), to: .tableau(destIndex), state: state) {
|
||||
hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .tableau(destIndex), priority: 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: Free cell to tableau
|
||||
for (cellIndex, cell) in state.freeCells.enumerated() {
|
||||
guard let card = cell else { continue }
|
||||
for (tabIndex, _) in state.tableaus.enumerated() {
|
||||
if canMove(cards: [card], from: .freeCell(cellIndex), to: .tableau(tabIndex), state: state) {
|
||||
hints.append(HintResult(cards: [card], from: .freeCell(cellIndex), to: .tableau(tabIndex), priority: 4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 5: Move to free cell
|
||||
if state.freeCells.contains(where: { $0 == nil }) {
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
guard let topCard = tableau.last else { continue }
|
||||
if let emptyCell = state.freeCells.firstIndex(where: { $0 == nil }) {
|
||||
hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .freeCell(emptyCell), priority: 5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hints.sorted { $0.priority < $1.priority }
|
||||
}
|
||||
|
||||
func canStackOnTableau(card: Card, onto target: Card) -> Bool {
|
||||
MoveValidator.isAlternatingColor(card, with: target)
|
||||
&& MoveValidator.isDescending(card, onto: target)
|
||||
}
|
||||
|
||||
func canPickUp(cards: [Card], from: CardLocation, state: GameSnapshot) -> Bool {
|
||||
guard !cards.isEmpty else { return false }
|
||||
|
||||
switch from {
|
||||
case .tableau(let index):
|
||||
guard index >= 0, index < state.tableaus.count else { return false }
|
||||
// All cards must be face-up and form a valid alternating-color descending sequence
|
||||
guard cards.allSatisfy({ $0.isFaceUp }) else { return false }
|
||||
for i in 1..<cards.count {
|
||||
guard MoveValidator.isAlternatingColor(cards[i], with: cards[i - 1]),
|
||||
MoveValidator.isDescending(cards[i], onto: cards[i - 1]) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case .freeCell(let index):
|
||||
guard cards.count == 1, index >= 0, index < state.freeCells.count else { return false }
|
||||
return state.freeCells[index] == cards.first
|
||||
|
||||
case .foundation(let index):
|
||||
guard cards.count == 1, index >= 0, index < state.foundations.count else { return false }
|
||||
return cards[0] == state.foundations[index].last
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
protocol GameRules: Sendable {
|
||||
var variant: GameVariant { get }
|
||||
|
||||
/// Deal cards from a shuffled deck into the initial board layout.
|
||||
func deal(deck: [Card]) -> GameSnapshot
|
||||
|
||||
/// Check if moving the given cards from source to destination is valid.
|
||||
func canMove(cards: [Card], from: CardLocation, to: CardLocation, state: GameSnapshot) -> Bool
|
||||
|
||||
/// Return all valid destinations for the given cards from a source location.
|
||||
func validDestinations(for cards: [Card], from: CardLocation, state: GameSnapshot) -> [CardLocation]
|
||||
|
||||
/// Draw card(s) from the stock pile.
|
||||
func drawFromStock(state: inout GameSnapshot, drawCount: Int) -> MoveAction?
|
||||
|
||||
/// Calculate the score change for a given move.
|
||||
func scoreForMove(from: CardLocation, to: CardLocation) -> Int
|
||||
|
||||
/// Check if the game is won.
|
||||
func isWon(state: GameSnapshot) -> Bool
|
||||
|
||||
/// Check if auto-complete is possible (all remaining cards are face-up and ordered).
|
||||
func canAutoComplete(state: GameSnapshot) -> Bool
|
||||
|
||||
/// Find available hints, ordered by priority (1 = highest).
|
||||
func findHints(state: GameSnapshot, settings: DifficultySettings) -> [HintResult]
|
||||
|
||||
/// Check if a card can be stacked onto another card on a tableau.
|
||||
func canStackOnTableau(card: Card, onto target: Card) -> Bool
|
||||
|
||||
/// Check if a group of cards can be picked up from a location.
|
||||
func canPickUp(cards: [Card], from: CardLocation, state: GameSnapshot) -> Bool
|
||||
}
|
||||
|
||||
extension GameRules {
|
||||
func validDestinations(for cards: [Card], from: CardLocation, state: GameSnapshot) -> [CardLocation] {
|
||||
var destinations: [CardLocation] = []
|
||||
|
||||
for i in 0..<state.foundations.count {
|
||||
if canMove(cards: cards, from: from, to: .foundation(i), state: state) {
|
||||
destinations.append(.foundation(i))
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..<state.tableaus.count {
|
||||
if canMove(cards: cards, from: from, to: .tableau(i), state: state) {
|
||||
destinations.append(.tableau(i))
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..<state.freeCells.count {
|
||||
if canMove(cards: cards, from: from, to: .freeCell(i), state: state) {
|
||||
destinations.append(.freeCell(i))
|
||||
}
|
||||
}
|
||||
|
||||
return destinations
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum GameRulesFactory {
|
||||
static func rules(for variant: GameVariant) -> GameRules {
|
||||
switch variant {
|
||||
case .klondike: KlondikeRules()
|
||||
case .spider: SpiderRules()
|
||||
case .freeCell: FreeCellRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import Foundation
|
||||
import Observation
|
||||
|
||||
@Observable
|
||||
final class GameState {
|
||||
var tableaus: [[Card]]
|
||||
var foundations: [[Card]]
|
||||
var stock: [Card]
|
||||
var waste: [Card]
|
||||
var freeCells: [Card?]
|
||||
|
||||
var moves: Int = 0
|
||||
var score: Int = 0
|
||||
var phase: GamePhase = .notStarted
|
||||
|
||||
private(set) var history: [GameSnapshot] = []
|
||||
private let maxHistorySize = 20
|
||||
|
||||
var canUndo: Bool { !history.isEmpty }
|
||||
|
||||
init() {
|
||||
self.tableaus = []
|
||||
self.foundations = []
|
||||
self.stock = []
|
||||
self.waste = []
|
||||
self.freeCells = []
|
||||
}
|
||||
|
||||
func snapshot() -> GameSnapshot {
|
||||
GameSnapshot(
|
||||
tableaus: tableaus,
|
||||
foundations: foundations,
|
||||
stock: stock,
|
||||
waste: waste,
|
||||
freeCells: freeCells,
|
||||
moves: moves,
|
||||
score: score
|
||||
)
|
||||
}
|
||||
|
||||
func restore(from snapshot: GameSnapshot) {
|
||||
tableaus = snapshot.tableaus
|
||||
foundations = snapshot.foundations
|
||||
stock = snapshot.stock
|
||||
waste = snapshot.waste
|
||||
freeCells = snapshot.freeCells
|
||||
moves = snapshot.moves
|
||||
score = snapshot.score
|
||||
}
|
||||
|
||||
func pushHistory() {
|
||||
history.append(snapshot())
|
||||
if history.count > maxHistorySize {
|
||||
history.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
func popHistory() -> GameSnapshot? {
|
||||
history.popLast()
|
||||
}
|
||||
|
||||
func clearHistory() {
|
||||
history.removeAll()
|
||||
}
|
||||
|
||||
func reset(from snapshot: GameSnapshot, variant: GameVariant) {
|
||||
restore(from: snapshot)
|
||||
phase = .playing
|
||||
clearHistory()
|
||||
|
||||
if variant.hasFreeCells {
|
||||
freeCells = Array(repeating: nil, count: variant.freeCellCount)
|
||||
} else {
|
||||
freeCells = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct KlondikeRules: GameRules {
|
||||
let variant: GameVariant = .klondike
|
||||
|
||||
func deal(deck: [Card]) -> GameSnapshot {
|
||||
var remaining = deck
|
||||
var tableaus: [[Card]] = []
|
||||
|
||||
// Deal 7 tableaus: column i gets i+1 cards, top card face-up
|
||||
for i in 0..<7 {
|
||||
var column: [Card] = []
|
||||
for j in 0...i {
|
||||
var card = remaining.removeFirst()
|
||||
card.isFaceUp = (j == i)
|
||||
column.append(card)
|
||||
}
|
||||
tableaus.append(column)
|
||||
}
|
||||
|
||||
return GameSnapshot(
|
||||
tableaus: tableaus,
|
||||
foundations: [[], [], [], []],
|
||||
stock: remaining,
|
||||
waste: [],
|
||||
freeCells: [],
|
||||
moves: 0,
|
||||
score: 0
|
||||
)
|
||||
}
|
||||
|
||||
func canMove(cards: [Card], from: CardLocation, to: CardLocation, state: GameSnapshot) -> Bool {
|
||||
guard let firstCard = cards.first else { return false }
|
||||
|
||||
switch to {
|
||||
case .foundation(let index):
|
||||
guard cards.count == 1 else { return false }
|
||||
guard index >= 0, index < state.foundations.count else { return false }
|
||||
let foundation = state.foundations[index]
|
||||
return MoveValidator.canPlaceOnFoundation(firstCard, topCard: foundation.last)
|
||||
|
||||
case .tableau(let index):
|
||||
guard index >= 0, index < state.tableaus.count else { return false }
|
||||
let tableau = state.tableaus[index]
|
||||
if tableau.isEmpty {
|
||||
return MoveValidator.isKing(firstCard)
|
||||
}
|
||||
guard let topCard = tableau.last, topCard.isFaceUp else { return false }
|
||||
return MoveValidator.isAlternatingColor(firstCard, with: topCard)
|
||||
&& MoveValidator.isDescending(firstCard, onto: topCard)
|
||||
|
||||
case .waste, .stock, .freeCell:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func drawFromStock(state: inout GameSnapshot, drawCount: Int) -> MoveAction? {
|
||||
if state.stock.isEmpty {
|
||||
guard !state.waste.isEmpty else { return nil }
|
||||
// Reset: move waste back to stock, reversed
|
||||
state.stock = state.waste.reversed().map { card in
|
||||
var c = card
|
||||
c.isFaceUp = false
|
||||
return c
|
||||
}
|
||||
state.waste = []
|
||||
return MoveAction(cards: [], from: .waste, to: .stock, didFlipCard: false, scoreChange: 0)
|
||||
}
|
||||
|
||||
var drawn: [Card] = []
|
||||
for _ in 0..<min(drawCount, state.stock.count) {
|
||||
var card = state.stock.removeLast()
|
||||
card.isFaceUp = true
|
||||
drawn.append(card)
|
||||
}
|
||||
state.waste.append(contentsOf: drawn)
|
||||
return MoveAction(cards: drawn, from: .stock, to: .waste, didFlipCard: true, scoreChange: 0)
|
||||
}
|
||||
|
||||
func scoreForMove(from: CardLocation, to: CardLocation) -> Int {
|
||||
switch (from, to) {
|
||||
case (.waste, .foundation): return 10
|
||||
case (.tableau, .foundation): return 10
|
||||
case (.waste, .tableau): return 5
|
||||
case (.foundation, .tableau): return -15
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
func isWon(state: GameSnapshot) -> Bool {
|
||||
state.foundations.allSatisfy { $0.count == 13 }
|
||||
}
|
||||
|
||||
func canAutoComplete(state: GameSnapshot) -> Bool {
|
||||
guard state.stock.isEmpty, state.waste.isEmpty else { return false }
|
||||
// All tableau cards must be face-up
|
||||
return state.tableaus.allSatisfy { column in
|
||||
column.allSatisfy { $0.isFaceUp }
|
||||
}
|
||||
}
|
||||
|
||||
func findHints(state: GameSnapshot, settings: DifficultySettings) -> [HintResult] {
|
||||
guard settings.hintsEnabled else { return [] }
|
||||
var hints: [HintResult] = []
|
||||
|
||||
// Priority 1: Aces to foundation
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
guard let topCard = tableau.last, topCard.isFaceUp, MoveValidator.isAce(topCard) else { continue }
|
||||
for (fIndex, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(topCard, topCard: foundation.last) {
|
||||
hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .foundation(fIndex), priority: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 1: Ace from waste
|
||||
if let wasteTop = state.waste.last, MoveValidator.isAce(wasteTop) {
|
||||
for (fIndex, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(wasteTop, topCard: foundation.last) {
|
||||
hints.append(HintResult(cards: [wasteTop], from: .waste, to: .foundation(fIndex), priority: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Other foundation moves
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
guard let topCard = tableau.last, topCard.isFaceUp, !MoveValidator.isAce(topCard) else { continue }
|
||||
for (fIndex, foundation) in state.foundations.enumerated() {
|
||||
if MoveValidator.canPlaceOnFoundation(topCard, topCard: foundation.last) {
|
||||
hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .foundation(fIndex), priority: 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Tableau moves that reveal face-down cards
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
let faceUpCards = tableau.trailingSuffix(while: { $0.isFaceUp })
|
||||
guard !faceUpCards.isEmpty else { continue }
|
||||
let hasFaceDown = tableau.count > faceUpCards.count
|
||||
|
||||
guard hasFaceDown else { continue }
|
||||
|
||||
for (destIndex, _) in state.tableaus.enumerated() {
|
||||
guard destIndex != tabIndex else { continue }
|
||||
let cardsToMove = Array(faceUpCards)
|
||||
if canMove(cards: cardsToMove, from: .tableau(tabIndex), to: .tableau(destIndex), state: state) {
|
||||
hints.append(HintResult(cards: cardsToMove, from: .tableau(tabIndex), to: .tableau(destIndex), priority: 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: Waste to tableau
|
||||
if let wasteTop = state.waste.last {
|
||||
for (tabIndex, _) in state.tableaus.enumerated() {
|
||||
if canMove(cards: [wasteTop], from: .waste, to: .tableau(tabIndex), state: state) {
|
||||
hints.append(HintResult(cards: [wasteTop], from: .waste, to: .tableau(tabIndex), priority: 4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 5: Draw from stock
|
||||
if !state.stock.isEmpty {
|
||||
hints.append(HintResult(cards: [], from: .stock, to: .waste, priority: 5))
|
||||
}
|
||||
|
||||
return hints.sorted { $0.priority < $1.priority }
|
||||
}
|
||||
|
||||
func canStackOnTableau(card: Card, onto target: Card) -> Bool {
|
||||
MoveValidator.isAlternatingColor(card, with: target)
|
||||
&& MoveValidator.isDescending(card, onto: target)
|
||||
}
|
||||
|
||||
func canPickUp(cards: [Card], from: CardLocation, state: GameSnapshot) -> Bool {
|
||||
guard !cards.isEmpty else { return false }
|
||||
|
||||
switch from {
|
||||
case .waste:
|
||||
return cards.count == 1 && cards[0] == state.waste.last
|
||||
|
||||
case .tableau(let index):
|
||||
guard index >= 0, index < state.tableaus.count else { return false }
|
||||
let tableau = state.tableaus[index]
|
||||
guard cards.allSatisfy({ $0.isFaceUp }) else { return false }
|
||||
// Cards must be a valid sequence from the bottom of the face-up portion
|
||||
let faceUpCards = tableau.trailingSuffix(while: { $0.isFaceUp })
|
||||
guard cards.count <= faceUpCards.count else { return false }
|
||||
let startIdx = faceUpCards.count - cards.count
|
||||
let expectedCards = Array(faceUpCards.dropFirst(startIdx))
|
||||
return cards == expectedCards
|
||||
|
||||
case .foundation(let index):
|
||||
guard cards.count == 1, index >= 0, index < state.foundations.count else { return false }
|
||||
return cards[0] == state.foundations[index].last
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum MoveValidator {
|
||||
/// True if the two cards have alternating colors (one red, one black).
|
||||
static func isAlternatingColor(_ card: Card, with other: Card) -> Bool {
|
||||
card.color != other.color
|
||||
}
|
||||
|
||||
/// True if both cards share the same suit.
|
||||
static func isSameSuit(_ card: Card, as other: Card) -> Bool {
|
||||
card.suit == other.suit
|
||||
}
|
||||
|
||||
/// True if card's rank is exactly one higher than target (for foundation building A→K).
|
||||
static func isAscending(_ card: Card, onto target: Card) -> Bool {
|
||||
card.rank.rawValue == target.rank.rawValue + 1
|
||||
}
|
||||
|
||||
/// True if card's rank is exactly one lower than target (for tableau stacking K→A).
|
||||
static func isDescending(_ card: Card, onto target: Card) -> Bool {
|
||||
card.rank.rawValue == target.rank.rawValue - 1
|
||||
}
|
||||
|
||||
/// True if the card is an Ace.
|
||||
static func isAce(_ card: Card) -> Bool {
|
||||
card.rank == .ace
|
||||
}
|
||||
|
||||
/// True if the card is a King.
|
||||
static func isKing(_ card: Card) -> Bool {
|
||||
card.rank == .king
|
||||
}
|
||||
|
||||
/// True if the card can be placed on a foundation pile (same suit, ascending).
|
||||
static func canPlaceOnFoundation(_ card: Card, topCard: Card?) -> Bool {
|
||||
guard let topCard else {
|
||||
return isAce(card)
|
||||
}
|
||||
return isSameSuit(card, as: topCard) && isAscending(card, onto: topCard)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct SpiderRules: GameRules {
|
||||
let variant: GameVariant = .spider
|
||||
|
||||
func deal(deck: [Card]) -> GameSnapshot {
|
||||
var remaining = deck
|
||||
var tableaus: [[Card]] = []
|
||||
|
||||
// Deal 10 tableaus: first 4 get 6 cards, last 6 get 5 cards, top face-up
|
||||
for i in 0..<10 {
|
||||
let cardCount = i < 4 ? 6 : 5
|
||||
var column: [Card] = []
|
||||
for j in 0..<cardCount {
|
||||
var card = remaining.removeFirst()
|
||||
card.isFaceUp = (j == cardCount - 1)
|
||||
column.append(card)
|
||||
}
|
||||
tableaus.append(column)
|
||||
}
|
||||
|
||||
return GameSnapshot(
|
||||
tableaus: tableaus,
|
||||
foundations: Array(repeating: [], count: 8),
|
||||
stock: remaining,
|
||||
waste: [],
|
||||
freeCells: [],
|
||||
moves: 0,
|
||||
score: 0
|
||||
)
|
||||
}
|
||||
|
||||
func canMove(cards: [Card], from: CardLocation, to: CardLocation, state: GameSnapshot) -> Bool {
|
||||
guard let firstCard = cards.first else { return false }
|
||||
|
||||
switch to {
|
||||
case .tableau(let index):
|
||||
guard index >= 0, index < state.tableaus.count else { return false }
|
||||
let tableau = state.tableaus[index]
|
||||
if tableau.isEmpty { return true }
|
||||
guard let topCard = tableau.last, topCard.isFaceUp else { return false }
|
||||
return MoveValidator.isDescending(firstCard, onto: topCard)
|
||||
|
||||
case .foundation:
|
||||
// Foundations are auto-filled when a complete K→A same-suit sequence is formed
|
||||
return false
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func drawFromStock(state: inout GameSnapshot, drawCount: Int) -> MoveAction? {
|
||||
// Spider deals one card to each tableau (must have at least one card per tableau)
|
||||
guard !state.stock.isEmpty else { return nil }
|
||||
guard state.tableaus.allSatisfy({ !$0.isEmpty }) else { return nil }
|
||||
|
||||
var drawn: [Card] = []
|
||||
let dealCount = min(state.tableaus.count, state.stock.count)
|
||||
for i in 0..<dealCount {
|
||||
var card = state.stock.removeLast()
|
||||
card.isFaceUp = true
|
||||
state.tableaus[i].append(card)
|
||||
drawn.append(card)
|
||||
}
|
||||
|
||||
return MoveAction(cards: drawn, from: .stock, to: .waste, didFlipCard: true, scoreChange: 0)
|
||||
}
|
||||
|
||||
func scoreForMove(from: CardLocation, to: CardLocation) -> Int {
|
||||
switch (from, to) {
|
||||
case (.tableau, .foundation): return 100
|
||||
case (.tableau, .tableau): return 1
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
func isWon(state: GameSnapshot) -> Bool {
|
||||
state.foundations.allSatisfy { $0.count == 13 }
|
||||
}
|
||||
|
||||
func canAutoComplete(state: GameSnapshot) -> Bool {
|
||||
guard state.stock.isEmpty else { return false }
|
||||
return state.tableaus.allSatisfy { column in
|
||||
column.allSatisfy { $0.isFaceUp }
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the top cards of a tableau form a complete K→A same-suit sequence.
|
||||
func isCompleteSequence(in tableau: [Card]) -> Bool {
|
||||
guard tableau.count >= 13 else { return false }
|
||||
let sequence = tableau.suffix(13)
|
||||
let suit = sequence.first!.suit
|
||||
|
||||
for (offset, card) in sequence.enumerated() {
|
||||
guard card.isFaceUp,
|
||||
card.suit == suit,
|
||||
card.rank.rawValue == 13 - offset else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/// Check all tableaus for complete sequences and move them to foundations.
|
||||
func checkAndMoveCompleteSequences(state: inout GameSnapshot) -> Bool {
|
||||
var foundComplete = false
|
||||
for tabIndex in 0..<state.tableaus.count {
|
||||
if isCompleteSequence(in: state.tableaus[tabIndex]) {
|
||||
let sequence = Array(state.tableaus[tabIndex].suffix(13))
|
||||
state.tableaus[tabIndex].removeLast(13)
|
||||
// Find first empty foundation
|
||||
if let fIndex = state.foundations.firstIndex(where: { $0.isEmpty }) {
|
||||
state.foundations[fIndex] = sequence
|
||||
foundComplete = true
|
||||
}
|
||||
// Flip new top card
|
||||
if let lastIndex = state.tableaus[tabIndex].indices.last,
|
||||
!state.tableaus[tabIndex][lastIndex].isFaceUp {
|
||||
state.tableaus[tabIndex][lastIndex].isFaceUp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundComplete
|
||||
}
|
||||
|
||||
func findHints(state: GameSnapshot, settings: DifficultySettings) -> [HintResult] {
|
||||
guard settings.hintsEnabled else { return [] }
|
||||
var hints: [HintResult] = []
|
||||
|
||||
for (tabIndex, tableau) in state.tableaus.enumerated() {
|
||||
let faceUpRun = sameSuitDescendingRun(in: tableau)
|
||||
guard !faceUpRun.isEmpty else { continue }
|
||||
|
||||
for (destIndex, _) in state.tableaus.enumerated() {
|
||||
guard destIndex != tabIndex else { continue }
|
||||
if canMove(cards: faceUpRun, from: .tableau(tabIndex), to: .tableau(destIndex), state: state) {
|
||||
let priority = (tableau.count - faceUpRun.count > 0 &&
|
||||
!tableau[tableau.count - faceUpRun.count - 1].isFaceUp) ? 2 : 4
|
||||
hints.append(HintResult(cards: faceUpRun, from: .tableau(tabIndex), to: .tableau(destIndex), priority: priority))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !state.stock.isEmpty {
|
||||
hints.append(HintResult(cards: [], from: .stock, to: .waste, priority: 5))
|
||||
}
|
||||
|
||||
return hints.sorted { $0.priority < $1.priority }
|
||||
}
|
||||
|
||||
func canStackOnTableau(card: Card, onto target: Card) -> Bool {
|
||||
MoveValidator.isDescending(card, onto: target)
|
||||
}
|
||||
|
||||
func canPickUp(cards: [Card], from: CardLocation, state: GameSnapshot) -> Bool {
|
||||
guard !cards.isEmpty else { return false }
|
||||
|
||||
switch from {
|
||||
case .tableau(let index):
|
||||
guard index >= 0, index < state.tableaus.count else { return false }
|
||||
guard cards.allSatisfy({ $0.isFaceUp }) else { return false }
|
||||
// Must be a same-suit descending sequence
|
||||
for i in 1..<cards.count {
|
||||
guard MoveValidator.isSameSuit(cards[i], as: cards[i - 1]),
|
||||
MoveValidator.isDescending(cards[i], onto: cards[i - 1]) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// Returns the longest same-suit descending run from the bottom of the face-up portion.
|
||||
private func sameSuitDescendingRun(in tableau: [Card]) -> [Card] {
|
||||
let faceUp = Array(tableau.trailingSuffix(while: { $0.isFaceUp }))
|
||||
guard !faceUp.isEmpty else { return [] }
|
||||
|
||||
var run = [faceUp.last!]
|
||||
for i in stride(from: faceUp.count - 2, through: 0, by: -1) {
|
||||
let card = faceUp[i]
|
||||
guard MoveValidator.isSameSuit(card, as: run[0]),
|
||||
card.rank.rawValue == run[0].rank.rawValue + 1 else {
|
||||
break
|
||||
}
|
||||
run.insert(card, at: 0)
|
||||
}
|
||||
return run
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct Card: Identifiable, Equatable, Hashable, Codable, Sendable {
|
||||
let id: UUID
|
||||
let suit: Suit
|
||||
let rank: Rank
|
||||
var isFaceUp: Bool
|
||||
|
||||
init(suit: Suit, rank: Rank, isFaceUp: Bool = false) {
|
||||
self.id = UUID()
|
||||
self.suit = suit
|
||||
self.rank = rank
|
||||
self.isFaceUp = isFaceUp
|
||||
}
|
||||
|
||||
var color: Suit.Color { suit.color }
|
||||
|
||||
var accessibilityDescription: String {
|
||||
if isFaceUp {
|
||||
"\(rank.displayName) of \(suit.displayName)"
|
||||
} else {
|
||||
"Card, face down"
|
||||
}
|
||||
}
|
||||
|
||||
func frontImageName(style: CardFaceStyle) -> String {
|
||||
"\(style.rawValue)_\(suit.rawValue)_\(rank.fileName)"
|
||||
}
|
||||
}
|
||||
|
||||
enum CardFaceStyle: String, CaseIterable, Codable, Sendable {
|
||||
case classic, modern, extracted
|
||||
}
|
||||
|
||||
enum CardBackDesign: Int, CaseIterable, Codable, Sendable {
|
||||
case abstractClouds = 1
|
||||
case abstractScene
|
||||
case abstract
|
||||
case astronaut
|
||||
case blue
|
||||
case blue2
|
||||
case cars
|
||||
case castle
|
||||
case fish
|
||||
case frog
|
||||
case red
|
||||
case red2
|
||||
|
||||
var imageName: String {
|
||||
"back_\(String(format: "%02d", rawValue))"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum CardLocation: Equatable, Hashable, Codable, Sendable {
|
||||
case tableau(Int)
|
||||
case foundation(Int)
|
||||
case waste
|
||||
case stock
|
||||
case freeCell(Int)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum Deck {
|
||||
/// Creates a standard 52-card deck, shuffled.
|
||||
static func standard() -> [Card] {
|
||||
var cards: [Card] = []
|
||||
for suit in Suit.allCases {
|
||||
for rank in Rank.allCases {
|
||||
cards.append(Card(suit: suit, rank: rank))
|
||||
}
|
||||
}
|
||||
cards.shuffle()
|
||||
return cards
|
||||
}
|
||||
|
||||
/// Creates a double deck (104 cards) for Spider solitaire, shuffled.
|
||||
static func double() -> [Card] {
|
||||
var cards: [Card] = []
|
||||
for _ in 0..<2 {
|
||||
for suit in Suit.allCases {
|
||||
for rank in Rank.allCases {
|
||||
cards.append(Card(suit: suit, rank: rank))
|
||||
}
|
||||
}
|
||||
}
|
||||
cards.shuffle()
|
||||
return cards
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum Difficulty: String, CaseIterable, Codable, Identifiable, Sendable {
|
||||
case easy, medium, hard, expert
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String { rawValue.capitalized }
|
||||
|
||||
var settings: DifficultySettings {
|
||||
switch self {
|
||||
case .easy:
|
||||
DifficultySettings(
|
||||
drawCount: 1,
|
||||
maxUndos: .max,
|
||||
hintsEnabled: true,
|
||||
scoreMultiplier: 0.5,
|
||||
timePenalty: 0,
|
||||
autoFlipDelay: 0.5
|
||||
)
|
||||
case .medium:
|
||||
DifficultySettings(
|
||||
drawCount: 3,
|
||||
maxUndos: 20,
|
||||
hintsEnabled: true,
|
||||
scoreMultiplier: 1.0,
|
||||
timePenalty: 2,
|
||||
autoFlipDelay: 0.3
|
||||
)
|
||||
case .hard:
|
||||
DifficultySettings(
|
||||
drawCount: 3,
|
||||
maxUndos: 10,
|
||||
hintsEnabled: false,
|
||||
scoreMultiplier: 1.5,
|
||||
timePenalty: 5,
|
||||
autoFlipDelay: 0.2
|
||||
)
|
||||
case .expert:
|
||||
DifficultySettings(
|
||||
drawCount: 3,
|
||||
maxUndos: 5,
|
||||
hintsEnabled: false,
|
||||
scoreMultiplier: 2.0,
|
||||
timePenalty: 10,
|
||||
autoFlipDelay: 0.1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DifficultySettings: Sendable {
|
||||
let drawCount: Int
|
||||
let maxUndos: Int
|
||||
let hintsEnabled: Bool
|
||||
let scoreMultiplier: Double
|
||||
let timePenalty: Int
|
||||
let autoFlipDelay: TimeInterval
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum GamePhase: Equatable, Sendable {
|
||||
case notStarted
|
||||
case playing
|
||||
case paused
|
||||
case autoCompleting
|
||||
case won
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct GameSnapshot: Codable, Sendable {
|
||||
var tableaus: [[Card]]
|
||||
var foundations: [[Card]]
|
||||
var stock: [Card]
|
||||
var waste: [Card]
|
||||
var freeCells: [Card?]
|
||||
var moves: Int
|
||||
var score: Int
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum GameVariant: String, CaseIterable, Codable, Identifiable, Sendable {
|
||||
case klondike
|
||||
case spider
|
||||
case freeCell
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .klondike: "Klondike"
|
||||
case .spider: "Spider"
|
||||
case .freeCell: "FreeCell"
|
||||
}
|
||||
}
|
||||
|
||||
var tableauCount: Int {
|
||||
switch self {
|
||||
case .klondike: 7
|
||||
case .spider: 10
|
||||
case .freeCell: 8
|
||||
}
|
||||
}
|
||||
|
||||
var foundationCount: Int {
|
||||
switch self {
|
||||
case .klondike: 4
|
||||
case .spider: 8
|
||||
case .freeCell: 4
|
||||
}
|
||||
}
|
||||
|
||||
var deckCount: Int {
|
||||
switch self {
|
||||
case .klondike, .freeCell: 1
|
||||
case .spider: 2
|
||||
}
|
||||
}
|
||||
|
||||
var hasWaste: Bool {
|
||||
switch self {
|
||||
case .klondike, .spider: true
|
||||
case .freeCell: false
|
||||
}
|
||||
}
|
||||
|
||||
var hasFreeCells: Bool {
|
||||
self == .freeCell
|
||||
}
|
||||
|
||||
var freeCellCount: Int {
|
||||
switch self {
|
||||
case .freeCell: 4
|
||||
case .klondike, .spider: 0
|
||||
}
|
||||
}
|
||||
|
||||
var hasStock: Bool {
|
||||
switch self {
|
||||
case .klondike, .spider: true
|
||||
case .freeCell: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct HintResult: Sendable {
|
||||
let cards: [Card]
|
||||
let from: CardLocation
|
||||
let to: CardLocation
|
||||
let priority: Int
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct MoveAction: Codable, Sendable {
|
||||
let cards: [Card]
|
||||
let from: CardLocation
|
||||
let to: CardLocation
|
||||
let didFlipCard: Bool
|
||||
let scoreChange: Int
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum Rank: Int, CaseIterable, Codable, Comparable, Sendable {
|
||||
case ace = 1
|
||||
case two, three, four, five, six, seven
|
||||
case eight, nine, ten, jack, queen, king
|
||||
|
||||
static func < (lhs: Rank, rhs: Rank) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .ace: "Ace"
|
||||
case .two: "2"
|
||||
case .three: "3"
|
||||
case .four: "4"
|
||||
case .five: "5"
|
||||
case .six: "6"
|
||||
case .seven: "7"
|
||||
case .eight: "8"
|
||||
case .nine: "9"
|
||||
case .ten: "10"
|
||||
case .jack: "Jack"
|
||||
case .queen: "Queen"
|
||||
case .king: "King"
|
||||
}
|
||||
}
|
||||
|
||||
var shortName: String {
|
||||
switch self {
|
||||
case .ace: "A"
|
||||
case .jack: "J"
|
||||
case .queen: "Q"
|
||||
case .king: "K"
|
||||
default: "\(rawValue)"
|
||||
}
|
||||
}
|
||||
|
||||
/// File name component for asset lookup
|
||||
var fileName: String {
|
||||
switch self {
|
||||
case .ace: "ace"
|
||||
case .two: "2"
|
||||
case .three: "3"
|
||||
case .four: "4"
|
||||
case .five: "5"
|
||||
case .six: "6"
|
||||
case .seven: "7"
|
||||
case .eight: "8"
|
||||
case .nine: "9"
|
||||
case .ten: "10"
|
||||
case .jack: "jack"
|
||||
case .queen: "queen"
|
||||
case .king: "king"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum Suit: String, CaseIterable, Codable, Sendable {
|
||||
case spades, hearts, diamonds, clubs
|
||||
|
||||
enum Color: Sendable {
|
||||
case red, black
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .hearts, .diamonds: .red
|
||||
case .spades, .clubs: .black
|
||||
}
|
||||
}
|
||||
|
||||
var symbol: String {
|
||||
switch self {
|
||||
case .spades: "♠"
|
||||
case .hearts: "♥"
|
||||
case .diamonds: "♦"
|
||||
case .clubs: "♣"
|
||||
}
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
rawValue.capitalized
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class GameRecord {
|
||||
var variant: String
|
||||
var difficulty: String
|
||||
var snapshotData: Data
|
||||
var elapsedSeconds: Int
|
||||
var lastPlayedDate: Date
|
||||
|
||||
init(variant: GameVariant, difficulty: Difficulty, snapshot: GameSnapshot, elapsedSeconds: Int) {
|
||||
self.variant = variant.rawValue
|
||||
self.difficulty = difficulty.rawValue
|
||||
self.snapshotData = (try? JSONEncoder().encode(snapshot)) ?? Data()
|
||||
self.elapsedSeconds = elapsedSeconds
|
||||
self.lastPlayedDate = Date()
|
||||
}
|
||||
|
||||
var decodedSnapshot: GameSnapshot? {
|
||||
try? JSONDecoder().decode(GameSnapshot.self, from: snapshotData)
|
||||
}
|
||||
|
||||
var gameVariant: GameVariant? {
|
||||
GameVariant(rawValue: variant)
|
||||
}
|
||||
|
||||
var gameDifficulty: Difficulty? {
|
||||
Difficulty(rawValue: difficulty)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@MainActor
|
||||
final class PersistenceManager {
|
||||
private let modelContext: ModelContext
|
||||
|
||||
init(modelContext: ModelContext) {
|
||||
self.modelContext = modelContext
|
||||
}
|
||||
|
||||
// MARK: - Game State
|
||||
|
||||
func saveGame(variant: GameVariant, difficulty: Difficulty, snapshot: GameSnapshot, elapsedSeconds: Int) {
|
||||
// Delete any existing save for this variant
|
||||
deleteSavedGame(for: variant)
|
||||
|
||||
let record = GameRecord(variant: variant, difficulty: difficulty,
|
||||
snapshot: snapshot, elapsedSeconds: elapsedSeconds)
|
||||
modelContext.insert(record)
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
func loadSavedGame(for variant: GameVariant) -> GameRecord? {
|
||||
let variantRaw = variant.rawValue
|
||||
let descriptor = FetchDescriptor<GameRecord>(
|
||||
predicate: #Predicate { $0.variant == variantRaw },
|
||||
sortBy: [SortDescriptor(\.lastPlayedDate, order: .reverse)]
|
||||
)
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
func loadMostRecentGame() -> GameRecord? {
|
||||
let descriptor = FetchDescriptor<GameRecord>(
|
||||
sortBy: [SortDescriptor(\.lastPlayedDate, order: .reverse)]
|
||||
)
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
func deleteSavedGame(for variant: GameVariant) {
|
||||
let variantRaw = variant.rawValue
|
||||
let descriptor = FetchDescriptor<GameRecord>(
|
||||
predicate: #Predicate { $0.variant == variantRaw }
|
||||
)
|
||||
if let records = try? modelContext.fetch(descriptor) {
|
||||
for record in records {
|
||||
modelContext.delete(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Statistics
|
||||
|
||||
func recordWin(variant: GameVariant, difficulty: Difficulty, score: Int, time: Int) {
|
||||
let record = fetchOrCreateStats(variant: variant, difficulty: difficulty)
|
||||
record.recordWin(score: score, time: time)
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
func recordLoss(variant: GameVariant, difficulty: Difficulty) {
|
||||
let record = fetchOrCreateStats(variant: variant, difficulty: difficulty)
|
||||
record.recordLoss()
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
func fetchStats(variant: GameVariant, difficulty: Difficulty) -> StatsRecord? {
|
||||
let variantRaw = variant.rawValue
|
||||
let difficultyRaw = difficulty.rawValue
|
||||
let descriptor = FetchDescriptor<StatsRecord>(
|
||||
predicate: #Predicate { $0.variant == variantRaw && $0.difficulty == difficultyRaw }
|
||||
)
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
private func fetchOrCreateStats(variant: GameVariant, difficulty: Difficulty) -> StatsRecord {
|
||||
if let existing = fetchStats(variant: variant, difficulty: difficulty) {
|
||||
return existing
|
||||
}
|
||||
let record = StatsRecord(variant: variant, difficulty: difficulty)
|
||||
modelContext.insert(record)
|
||||
return record
|
||||
}
|
||||
|
||||
// MARK: - Preferences
|
||||
|
||||
func loadPreferences() -> PrefsRecord {
|
||||
let descriptor = FetchDescriptor<PrefsRecord>()
|
||||
if let existing = try? modelContext.fetch(descriptor).first {
|
||||
return existing
|
||||
}
|
||||
let record = PrefsRecord()
|
||||
modelContext.insert(record)
|
||||
try? modelContext.save()
|
||||
return record
|
||||
}
|
||||
|
||||
func savePreferences(themeId: String, cardFaceStyle: CardFaceStyle,
|
||||
cardBackDesign: CardBackDesign, soundEnabled: Bool,
|
||||
lastVariant: GameVariant, lastDifficulty: Difficulty) {
|
||||
let prefs = loadPreferences()
|
||||
prefs.themeId = themeId
|
||||
prefs.cardFaceStyle = cardFaceStyle.rawValue
|
||||
prefs.cardBackDesign = cardBackDesign.rawValue
|
||||
prefs.soundEnabled = soundEnabled
|
||||
prefs.lastVariant = lastVariant.rawValue
|
||||
prefs.lastDifficulty = lastDifficulty.rawValue
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class PrefsRecord {
|
||||
var themeId: String
|
||||
var cardFaceStyle: String
|
||||
var cardBackDesign: Int
|
||||
var soundEnabled: Bool
|
||||
var lastVariant: String
|
||||
var lastDifficulty: String
|
||||
|
||||
init() {
|
||||
self.themeId = GameTheme.classicGreen.id
|
||||
self.cardFaceStyle = CardFaceStyle.classic.rawValue
|
||||
self.cardBackDesign = CardBackDesign.blue.rawValue
|
||||
self.soundEnabled = true
|
||||
self.lastVariant = GameVariant.klondike.rawValue
|
||||
self.lastDifficulty = Difficulty.medium.rawValue
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class StatsRecord {
|
||||
var variant: String
|
||||
var difficulty: String
|
||||
var gamesPlayed: Int
|
||||
var gamesWon: Int
|
||||
var bestScore: Int
|
||||
var bestTime: Int
|
||||
var currentStreak: Int
|
||||
var bestStreak: Int
|
||||
|
||||
init(variant: GameVariant, difficulty: Difficulty) {
|
||||
self.variant = variant.rawValue
|
||||
self.difficulty = difficulty.rawValue
|
||||
self.gamesPlayed = 0
|
||||
self.gamesWon = 0
|
||||
self.bestScore = 0
|
||||
self.bestTime = Int.max
|
||||
self.currentStreak = 0
|
||||
self.bestStreak = 0
|
||||
}
|
||||
|
||||
func recordWin(score: Int, time: Int) {
|
||||
gamesPlayed += 1
|
||||
gamesWon += 1
|
||||
if score > bestScore { bestScore = score }
|
||||
if time < bestTime { bestTime = time }
|
||||
currentStreak += 1
|
||||
if currentStreak > bestStreak { bestStreak = currentStreak }
|
||||
}
|
||||
|
||||
func recordLoss() {
|
||||
gamesPlayed += 1
|
||||
currentStreak = 0
|
||||
}
|
||||
|
||||
var winRate: Double {
|
||||
guard gamesPlayed > 0 else { return 0 }
|
||||
return Double(gamesWon) / Double(gamesPlayed)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 616 B |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 1 KiB |
|
|
@ -1,74 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon_512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_01.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 94 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_02.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 28 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_03.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 87 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_04.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 45 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_05.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 48 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_06.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 51 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_07.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 46 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_08.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 40 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_09.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_10.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_11.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 38 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "back_12.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 47 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_10.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_2.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.7 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_3.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_4.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.6 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_5.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_6.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_7.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_8.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_9.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_ace.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.1 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_jack.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 66 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_king.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 64 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_clubs_queen.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_diamonds_10.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.6 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_diamonds_2.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.5 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_diamonds_3.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.1 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_diamonds_4.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.2 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "classic_diamonds_5.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||