diff --git a/APP_STORE_METADATA.md b/APP_STORE_METADATA.md deleted file mode 100644 index b5b5df9..0000000 --- a/APP_STORE_METADATA.md +++ /dev/null @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 8b8ae19..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -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. diff --git a/SETUP.md b/SETUP.md deleted file mode 100644 index f15e488..0000000 --- a/SETUP.md +++ /dev/null @@ -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. diff --git a/SoliCards.xcodeproj/project.pbxproj b/SoliCards.xcodeproj/project.pbxproj deleted file mode 100644 index bb25564..0000000 --- a/SoliCards.xcodeproj/project.pbxproj +++ /dev/null @@ -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 = ""; }; - 0A61A63D3A6077A80ED09B24 /* TimerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerService.swift; sourceTree = ""; }; - 0B4ED587E94AC6FAE79FDAE8 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; - 0C4319F36333564A75A3FEB0 /* SpiderRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpiderRules.swift; sourceTree = ""; }; - 115321F0A0475538BA016151 /* AutoCompleter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleter.swift; sourceTree = ""; }; - 116E45BE9C698645A16CF6F3 /* Suit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suit.swift; sourceTree = ""; }; - 1391353BDCE5C135A9FA1F0A /* PrefsRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsRecord.swift; sourceTree = ""; }; - 16BEB4EF07C82D8B2A89D260 /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = ""; }; - 1B25C883D3B49CADA34775F1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 1EE2B9027264A1134D2B65DC /* CardStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardStackView.swift; sourceTree = ""; }; - 2065F4D438686C32E01E449F /* SpiderBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpiderBoardView.swift; sourceTree = ""; }; - 2123C4E1A2A72E80EC1123A5 /* SoliCardsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoliCardsApp.swift; sourceTree = ""; }; - 22BD39F68FBBFFB35FCB0EC5 /* KlondikeRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlondikeRules.swift; sourceTree = ""; }; - 23DEF0FCB8B782702D108DF3 /* ThemePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePickerView.swift; sourceTree = ""; }; - 25D311E1FE85DB24EA48404B /* DropTargetPreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropTargetPreferenceKey.swift; sourceTree = ""; }; - 26A1B8A570F6B4F573E29FCA /* GameRulesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRulesFactory.swift; sourceTree = ""; }; - 26F1DE1DBC8F3FFC732DA618 /* GameRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRules.swift; sourceTree = ""; }; - 2BF99A0055488CAE6E45D4A3 /* GameSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameSnapshot.swift; sourceTree = ""; }; - 2E568D8FC36EF5D9937E0F97 /* MoveAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveAction.swift; sourceTree = ""; }; - 396230FC7779389B46BE1246 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; - 3DD303449EB1172C9B52E624 /* SoundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundManager.swift; sourceTree = ""; }; - 403990D89B8854B6A45A44A2 /* KlondikeBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlondikeBoardView.swift; sourceTree = ""; }; - 488C04322FF37B821C662516 /* GamePhase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamePhase.swift; sourceTree = ""; }; - 4991B4E4B3E6B9AC2EDA629F /* GameRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRecord.swift; sourceTree = ""; }; - 4B7D93BC01A3997DD183D368 /* CardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardTests.swift; sourceTree = ""; }; - 4E2E837BB1ADC907A8F991A0 /* CardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardLayout.swift; sourceTree = ""; }; - 58632F749B766B0E79DD0152 /* DifficultyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DifficultyTests.swift; sourceTree = ""; }; - 5E14435814D19CE147D6D408 /* GameBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameBoardView.swift; sourceTree = ""; }; - 634429F416F1A484018E084E /* StatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsView.swift; sourceTree = ""; }; - 66617C01DF8B874B51A55295 /* StatsRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsRecord.swift; sourceTree = ""; }; - 6C9B49166D2E8BE0D9AA2B46 /* Rank.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rank.swift; sourceTree = ""; }; - 714F0FD173ED0BC8E680E85A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; - 72E42967DA3AA21AEE42F177 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; - 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 = ""; }; - 864D497FD148C1DCA5761247 /* HintResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HintResult.swift; sourceTree = ""; }; - 94A81A01510385FC02760FB4 /* AutoCompleterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleterTests.swift; sourceTree = ""; }; - 9810F299E93134997859B2BD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - A64A7B696016187523AD7277 /* Difficulty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Difficulty.swift; sourceTree = ""; }; - A7DAC2213D876C2E0F48CBB3 /* Array+Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Card.swift"; sourceTree = ""; }; - A8ABFB6829A8D0B281CCD3E9 /* FreeCellRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeCellRules.swift; sourceTree = ""; }; - A8E9415DE47EA69B52839CA5 /* GameStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStateTests.swift; sourceTree = ""; }; - AAA573DB9A21FE34ECFF6794 /* PersistenceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceManager.swift; sourceTree = ""; }; - AE7880E1D80183ABCFBF49F4 /* FreeCellBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeCellBoardView.swift; sourceTree = ""; }; - B1321C0FD7F644967F371B7A /* NewGameSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGameSheet.swift; sourceTree = ""; }; - B31155959181B8D516A82096 /* KlondikeRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlondikeRulesTests.swift; sourceTree = ""; }; - B8D4A7ACB3BD58FB808D7F89 /* StatsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsViewModel.swift; sourceTree = ""; }; - BC022E983E9801D0A3A448B4 /* SpiderRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpiderRulesTests.swift; sourceTree = ""; }; - C01DD3D6B3B0DEA17B94C81C /* DraggedCardsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedCardsOverlay.swift; sourceTree = ""; }; - C0B4677EC6C93B738F443D86 /* GameViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewModel.swift; sourceTree = ""; }; - C38D83697D866ACA495ED4B8 /* MoveValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveValidator.swift; sourceTree = ""; }; - C4901135D3529A476DEF089F /* MoveValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveValidatorTests.swift; sourceTree = ""; }; - C5C2150140E0A5A3C4CD18D0 /* ScoreBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreBarView.swift; sourceTree = ""; }; - C60AA0F17A17B13707CB0228 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; - C8A7187F45271D5F9FBB0A07 /* FreeCellRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeCellRulesTests.swift; sourceTree = ""; }; - CA7C39CE680C4AFC5D7959BC /* CardLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardLocation.swift; sourceTree = ""; }; - CB728E0216832C7887E0B0E4 /* CardBackPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBackPickerView.swift; sourceTree = ""; }; - CCE1E3886FE8A8FA0DD3C54A /* VictoryOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VictoryOverlayView.swift; sourceTree = ""; }; - CD2BABA5FED1321ABA0A2F55 /* GameTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameTheme.swift; sourceTree = ""; }; - CD3488A79053BB9BF5BF6C39 /* GameState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameState.swift; sourceTree = ""; }; - DA638FEAAD15EBE1E78FFE14 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; - DB81C7AA5EDF842B3D841907 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - E6AE9622A04A543946250427 /* MainMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuView.swift; sourceTree = ""; }; - EF9D48C269BE378EF381ADF6 /* CardStylePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardStylePickerView.swift; sourceTree = ""; }; - F31B3F7875D68C0BA0B8FB54 /* GameVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameVariant.swift; sourceTree = ""; }; - FDF0B14B437E3A3475278EC5 /* DeckTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckTests.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXGroup section */ - 08AF7AF8C77BED3EFA6A9AEA /* Statistics */ = { - isa = PBXGroup; - children = ( - 634429F416F1A484018E084E /* StatisticsView.swift */, - ); - path = Statistics; - sourceTree = ""; - }; - 0FF95606A40279B4D3390FBA = { - isa = PBXGroup; - children = ( - 2182AF78CEA93B7431E85608 /* SoliCards */, - D4605BF9A1E255A1F2E44B4F /* SoliCardsTests */, - 9CCB9EC5C71BA2DF24919C93 /* Products */, - ); - sourceTree = ""; - }; - 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 = ""; - }; - 2D71FE366319D21065F80CFD /* Persistence */ = { - isa = PBXGroup; - children = ( - 4991B4E4B3E6B9AC2EDA629F /* GameRecord.swift */, - AAA573DB9A21FE34ECFF6794 /* PersistenceManager.swift */, - 1391353BDCE5C135A9FA1F0A /* PrefsRecord.swift */, - 66617C01DF8B874B51A55295 /* StatsRecord.swift */, - ); - path = Persistence; - sourceTree = ""; - }; - 3271C08BF426FD74787C6217 /* Resources */ = { - isa = PBXGroup; - children = ( - DB81C7AA5EDF842B3D841907 /* Assets.xcassets */, - 714F0FD173ED0BC8E680E85A /* Localizable.xcstrings */, - 8595FFC9DDE3EA38CE0A617B /* PrivacyInfo.xcprivacy */, - AB3F10466EA26B5E0B5827A8 /* Sounds */, - ); - path = Resources; - sourceTree = ""; - }; - 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 = ""; - }; - 45A518EB42C0CDB893716BBE /* Settings */ = { - isa = PBXGroup; - children = ( - CB728E0216832C7887E0B0E4 /* CardBackPickerView.swift */, - EF9D48C269BE378EF381ADF6 /* CardStylePickerView.swift */, - 1B25C883D3B49CADA34775F1 /* SettingsView.swift */, - 23DEF0FCB8B782702D108DF3 /* ThemePickerView.swift */, - ); - path = Settings; - sourceTree = ""; - }; - 7A41E8DA0C1CCBC6C102E3A0 /* ViewModels */ = { - isa = PBXGroup; - children = ( - ); - path = ViewModels; - sourceTree = ""; - }; - 9CCB9EC5C71BA2DF24919C93 /* Products */ = { - isa = PBXGroup; - children = ( - 7561129FE301D2A5E3652648 /* SoliCards.app */, - 7E57D0CF303B42DD18A317F1 /* SoliCardsTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - A1D36CB5F370E4F3906E319A /* Services */ = { - isa = PBXGroup; - children = ( - 0B4ED587E94AC6FAE79FDAE8 /* HapticManager.swift */, - 3DD303449EB1172C9B52E624 /* SoundManager.swift */, - 0A61A63D3A6077A80ED09B24 /* TimerService.swift */, - ); - path = Services; - sourceTree = ""; - }; - A5353C257EB10EB9526C6E39 /* Menu */ = { - isa = PBXGroup; - children = ( - E6AE9622A04A543946250427 /* MainMenuView.swift */, - B1321C0FD7F644967F371B7A /* NewGameSheet.swift */, - 04FAD6B4305ABFB4769DE792 /* RulesView.swift */, - ); - path = Menu; - sourceTree = ""; - }; - A8376EC525EE5E88D4517BA1 /* Theme */ = { - isa = PBXGroup; - children = ( - CD2BABA5FED1321ABA0A2F55 /* GameTheme.swift */, - 396230FC7779389B46BE1246 /* ThemeManager.swift */, - ); - path = Theme; - sourceTree = ""; - }; - AB3F10466EA26B5E0B5827A8 /* Sounds */ = { - isa = PBXGroup; - children = ( - ); - path = Sounds; - sourceTree = ""; - }; - C6C973AB5DCD1C0E766F633A /* ViewModels */ = { - isa = PBXGroup; - children = ( - C0B4677EC6C93B738F443D86 /* GameViewModel.swift */, - C60AA0F17A17B13707CB0228 /* SettingsViewModel.swift */, - B8D4A7ACB3BD58FB808D7F89 /* StatsViewModel.swift */, - ); - path = ViewModels; - sourceTree = ""; - }; - D4605BF9A1E255A1F2E44B4F /* SoliCardsTests */ = { - isa = PBXGroup; - children = ( - DB86ADC0F0581A7B56EBE28E /* GameEngine */, - E5E67911DEBB2793B3D8F1AD /* Models */, - 7A41E8DA0C1CCBC6C102E3A0 /* ViewModels */, - ); - path = SoliCardsTests; - sourceTree = ""; - }; - 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 = ""; - }; - 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 = ""; - }; - E4D19DAEAABB74B9FDD84B73 /* Extensions */ = { - isa = PBXGroup; - children = ( - A7DAC2213D876C2E0F48CBB3 /* Array+Card.swift */, - 4E2E837BB1ADC907A8F991A0 /* CardLayout.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - E5E67911DEBB2793B3D8F1AD /* Models */ = { - isa = PBXGroup; - children = ( - 4B7D93BC01A3997DD183D368 /* CardTests.swift */, - FDF0B14B437E3A3475278EC5 /* DeckTests.swift */, - 58632F749B766B0E79DD0152 /* DifficultyTests.swift */, - ); - path = Models; - sourceTree = ""; - }; - F84CF6704CBB01C0C9875CE2 /* Views */ = { - isa = PBXGroup; - children = ( - DA0714078CFC7F8BCD993F8C /* Game */, - A5353C257EB10EB9526C6E39 /* Menu */, - 45A518EB42C0CDB893716BBE /* Settings */, - 08AF7AF8C77BED3EFA6A9AEA /* Statistics */, - ); - path = Views; - sourceTree = ""; - }; - 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 = ""; - }; -/* 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 */; -} diff --git a/SoliCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SoliCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/SoliCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/SoliCards/ContentView.swift b/SoliCards/ContentView.swift deleted file mode 100644 index 3fd34f9..0000000 --- a/SoliCards/ContentView.swift +++ /dev/null @@ -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") - } - } -} diff --git a/SoliCards/Extensions/Array+Card.swift b/SoliCards/Extensions/Array+Card.swift deleted file mode 100644 index b964342..0000000 --- a/SoliCards/Extensions/Array+Card.swift +++ /dev/null @@ -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 { - var startIndex = endIndex - for index in indices.reversed() { - guard predicate(self[index]) else { break } - startIndex = index - } - return self[startIndex.. 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 - } -} diff --git a/SoliCards/GameEngine/AutoCompleter.swift b/SoliCards/GameEngine/AutoCompleter.swift deleted file mode 100644 index dc29dc6..0000000 --- a/SoliCards/GameEngine/AutoCompleter.swift +++ /dev/null @@ -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 - } -} diff --git a/SoliCards/GameEngine/FreeCellRules.swift b/SoliCards/GameEngine/FreeCellRules.swift deleted file mode 100644 index fb2732f..0000000 --- a/SoliCards/GameEngine/FreeCellRules.swift +++ /dev/null @@ -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.. 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.. 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..= 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 - } - } -} diff --git a/SoliCards/GameEngine/GameRules.swift b/SoliCards/GameEngine/GameRules.swift deleted file mode 100644 index bc68b70..0000000 --- a/SoliCards/GameEngine/GameRules.swift +++ /dev/null @@ -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.. GameRules { - switch variant { - case .klondike: KlondikeRules() - case .spider: SpiderRules() - case .freeCell: FreeCellRules() - } - } -} diff --git a/SoliCards/GameEngine/GameState.swift b/SoliCards/GameEngine/GameState.swift deleted file mode 100644 index dc2969f..0000000 --- a/SoliCards/GameEngine/GameState.swift +++ /dev/null @@ -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 = [] - } - } -} diff --git a/SoliCards/GameEngine/KlondikeRules.swift b/SoliCards/GameEngine/KlondikeRules.swift deleted file mode 100644 index 90138af..0000000 --- a/SoliCards/GameEngine/KlondikeRules.swift +++ /dev/null @@ -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.. 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 - } - } -} diff --git a/SoliCards/GameEngine/MoveValidator.swift b/SoliCards/GameEngine/MoveValidator.swift deleted file mode 100644 index 5b8c4c6..0000000 --- a/SoliCards/GameEngine/MoveValidator.swift +++ /dev/null @@ -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) - } -} diff --git a/SoliCards/GameEngine/SpiderRules.swift b/SoliCards/GameEngine/SpiderRules.swift deleted file mode 100644 index 2a8a55d..0000000 --- a/SoliCards/GameEngine/SpiderRules.swift +++ /dev/null @@ -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.. 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.. 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.. [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.. [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 - } -} diff --git a/SoliCards/Models/Card.swift b/SoliCards/Models/Card.swift deleted file mode 100644 index 40060fe..0000000 --- a/SoliCards/Models/Card.swift +++ /dev/null @@ -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))" - } -} diff --git a/SoliCards/Models/CardLocation.swift b/SoliCards/Models/CardLocation.swift deleted file mode 100644 index 9f837e5..0000000 --- a/SoliCards/Models/CardLocation.swift +++ /dev/null @@ -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) -} diff --git a/SoliCards/Models/Deck.swift b/SoliCards/Models/Deck.swift deleted file mode 100644 index acee203..0000000 --- a/SoliCards/Models/Deck.swift +++ /dev/null @@ -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 - } -} diff --git a/SoliCards/Models/Difficulty.swift b/SoliCards/Models/Difficulty.swift deleted file mode 100644 index 73dce3f..0000000 --- a/SoliCards/Models/Difficulty.swift +++ /dev/null @@ -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 -} diff --git a/SoliCards/Models/GamePhase.swift b/SoliCards/Models/GamePhase.swift deleted file mode 100644 index 5bb3ebc..0000000 --- a/SoliCards/Models/GamePhase.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -enum GamePhase: Equatable, Sendable { - case notStarted - case playing - case paused - case autoCompleting - case won -} diff --git a/SoliCards/Models/GameSnapshot.swift b/SoliCards/Models/GameSnapshot.swift deleted file mode 100644 index 9488204..0000000 --- a/SoliCards/Models/GameSnapshot.swift +++ /dev/null @@ -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 -} diff --git a/SoliCards/Models/GameVariant.swift b/SoliCards/Models/GameVariant.swift deleted file mode 100644 index 5fdf00d..0000000 --- a/SoliCards/Models/GameVariant.swift +++ /dev/null @@ -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 - } - } -} diff --git a/SoliCards/Models/HintResult.swift b/SoliCards/Models/HintResult.swift deleted file mode 100644 index 0c8dc35..0000000 --- a/SoliCards/Models/HintResult.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -struct HintResult: Sendable { - let cards: [Card] - let from: CardLocation - let to: CardLocation - let priority: Int -} diff --git a/SoliCards/Models/MoveAction.swift b/SoliCards/Models/MoveAction.swift deleted file mode 100644 index 7cc5fda..0000000 --- a/SoliCards/Models/MoveAction.swift +++ /dev/null @@ -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 -} diff --git a/SoliCards/Models/Rank.swift b/SoliCards/Models/Rank.swift deleted file mode 100644 index 2aab4eb..0000000 --- a/SoliCards/Models/Rank.swift +++ /dev/null @@ -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" - } - } -} diff --git a/SoliCards/Models/Suit.swift b/SoliCards/Models/Suit.swift deleted file mode 100644 index 98aef7f..0000000 --- a/SoliCards/Models/Suit.swift +++ /dev/null @@ -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 - } -} diff --git a/SoliCards/Persistence/GameRecord.swift b/SoliCards/Persistence/GameRecord.swift deleted file mode 100644 index 3a29c6f..0000000 --- a/SoliCards/Persistence/GameRecord.swift +++ /dev/null @@ -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) - } -} diff --git a/SoliCards/Persistence/PersistenceManager.swift b/SoliCards/Persistence/PersistenceManager.swift deleted file mode 100644 index 005c66a..0000000 --- a/SoliCards/Persistence/PersistenceManager.swift +++ /dev/null @@ -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( - predicate: #Predicate { $0.variant == variantRaw }, - sortBy: [SortDescriptor(\.lastPlayedDate, order: .reverse)] - ) - return try? modelContext.fetch(descriptor).first - } - - func loadMostRecentGame() -> GameRecord? { - let descriptor = FetchDescriptor( - sortBy: [SortDescriptor(\.lastPlayedDate, order: .reverse)] - ) - return try? modelContext.fetch(descriptor).first - } - - func deleteSavedGame(for variant: GameVariant) { - let variantRaw = variant.rawValue - let descriptor = FetchDescriptor( - 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( - 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() - 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() - } -} diff --git a/SoliCards/Persistence/PrefsRecord.swift b/SoliCards/Persistence/PrefsRecord.swift deleted file mode 100644 index f5dce0e..0000000 --- a/SoliCards/Persistence/PrefsRecord.swift +++ /dev/null @@ -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 - } -} diff --git a/SoliCards/Persistence/StatsRecord.swift b/SoliCards/Persistence/StatsRecord.swift deleted file mode 100644 index 213dc2b..0000000 --- a/SoliCards/Persistence/StatsRecord.swift +++ /dev/null @@ -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) - } -} diff --git a/SoliCards/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/SoliCards/Resources/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/SoliCards/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png deleted file mode 100644 index eca79a6..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_128.png b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_128.png deleted file mode 100644 index 2f22373..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_128.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_16.png b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_16.png deleted file mode 100644 index 7a01c1c..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_16.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_256.png b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_256.png deleted file mode 100644 index 836bc38..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_256.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_32.png b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_32.png deleted file mode 100644 index 74040fe..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_32.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_512.png b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_512.png deleted file mode 100644 index 5bd81e6..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_512.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_64.png b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_64.png deleted file mode 100644 index 2b0dd2e..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_64.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9d1f6e8..0000000 --- a/SoliCards/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -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 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/Contents.json b/SoliCards/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/SoliCards/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_01.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_01.imageset/Contents.json deleted file mode 100644 index 3c3809b..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_01.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_01.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_01.imageset/back_01.png b/SoliCards/Resources/Assets.xcassets/back_01.imageset/back_01.png deleted file mode 100644 index 563556a..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_01.imageset/back_01.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_02.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_02.imageset/Contents.json deleted file mode 100644 index 1612a17..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_02.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_02.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_02.imageset/back_02.png b/SoliCards/Resources/Assets.xcassets/back_02.imageset/back_02.png deleted file mode 100644 index d85e83b..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_02.imageset/back_02.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_03.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_03.imageset/Contents.json deleted file mode 100644 index 49ffafb..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_03.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_03.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_03.imageset/back_03.png b/SoliCards/Resources/Assets.xcassets/back_03.imageset/back_03.png deleted file mode 100644 index 47c9b3b..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_03.imageset/back_03.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_04.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_04.imageset/Contents.json deleted file mode 100644 index 230cb4f..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_04.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_04.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_04.imageset/back_04.png b/SoliCards/Resources/Assets.xcassets/back_04.imageset/back_04.png deleted file mode 100644 index 87eeee6..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_04.imageset/back_04.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_05.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_05.imageset/Contents.json deleted file mode 100644 index af24007..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_05.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_05.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_05.imageset/back_05.png b/SoliCards/Resources/Assets.xcassets/back_05.imageset/back_05.png deleted file mode 100644 index f24b21a..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_05.imageset/back_05.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_06.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_06.imageset/Contents.json deleted file mode 100644 index 32c1b0f..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_06.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_06.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_06.imageset/back_06.png b/SoliCards/Resources/Assets.xcassets/back_06.imageset/back_06.png deleted file mode 100644 index 313e608..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_06.imageset/back_06.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_07.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_07.imageset/Contents.json deleted file mode 100644 index 00cb6a0..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_07.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_07.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_07.imageset/back_07.png b/SoliCards/Resources/Assets.xcassets/back_07.imageset/back_07.png deleted file mode 100644 index 0eab442..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_07.imageset/back_07.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_08.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_08.imageset/Contents.json deleted file mode 100644 index 7c39174..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_08.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_08.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_08.imageset/back_08.png b/SoliCards/Resources/Assets.xcassets/back_08.imageset/back_08.png deleted file mode 100644 index dad8d95..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_08.imageset/back_08.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_09.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_09.imageset/Contents.json deleted file mode 100644 index 59e48ae..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_09.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_09.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_09.imageset/back_09.png b/SoliCards/Resources/Assets.xcassets/back_09.imageset/back_09.png deleted file mode 100644 index f10ad18..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_09.imageset/back_09.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_10.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_10.imageset/Contents.json deleted file mode 100644 index e1aab8e..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_10.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_10.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_10.imageset/back_10.png b/SoliCards/Resources/Assets.xcassets/back_10.imageset/back_10.png deleted file mode 100644 index 81f4f77..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_10.imageset/back_10.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_11.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_11.imageset/Contents.json deleted file mode 100644 index 66bf304..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_11.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_11.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_11.imageset/back_11.png b/SoliCards/Resources/Assets.xcassets/back_11.imageset/back_11.png deleted file mode 100644 index 176afd7..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_11.imageset/back_11.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/back_12.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/back_12.imageset/Contents.json deleted file mode 100644 index e29b561..0000000 --- a/SoliCards/Resources/Assets.xcassets/back_12.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "back_12.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/back_12.imageset/back_12.png b/SoliCards/Resources/Assets.xcassets/back_12.imageset/back_12.png deleted file mode 100644 index 5160634..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/back_12.imageset/back_12.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_10.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_10.imageset/Contents.json deleted file mode 100644 index cadbb8f..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_10.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_10.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_10.imageset/classic_clubs_10.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_10.imageset/classic_clubs_10.png deleted file mode 100644 index f9158e9..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_10.imageset/classic_clubs_10.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_2.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_2.imageset/Contents.json deleted file mode 100644 index 35c9414..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_2.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_2.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_2.imageset/classic_clubs_2.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_2.imageset/classic_clubs_2.png deleted file mode 100644 index e655ea9..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_2.imageset/classic_clubs_2.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_3.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_3.imageset/Contents.json deleted file mode 100644 index f16e5dc..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_3.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_3.imageset/classic_clubs_3.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_3.imageset/classic_clubs_3.png deleted file mode 100644 index ace0ff5..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_3.imageset/classic_clubs_3.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_4.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_4.imageset/Contents.json deleted file mode 100644 index c8af9c0..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_4.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_4.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_4.imageset/classic_clubs_4.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_4.imageset/classic_clubs_4.png deleted file mode 100644 index 9d58b9e..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_4.imageset/classic_clubs_4.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_5.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_5.imageset/Contents.json deleted file mode 100644 index 321ee4e..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_5.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_5.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_5.imageset/classic_clubs_5.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_5.imageset/classic_clubs_5.png deleted file mode 100644 index 37d870c..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_5.imageset/classic_clubs_5.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_6.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_6.imageset/Contents.json deleted file mode 100644 index fd86cb6..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_6.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_6.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_6.imageset/classic_clubs_6.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_6.imageset/classic_clubs_6.png deleted file mode 100644 index f2da6b4..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_6.imageset/classic_clubs_6.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_7.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_7.imageset/Contents.json deleted file mode 100644 index 630b2ee..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_7.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_7.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_7.imageset/classic_clubs_7.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_7.imageset/classic_clubs_7.png deleted file mode 100644 index 8de0a4e..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_7.imageset/classic_clubs_7.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_8.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_8.imageset/Contents.json deleted file mode 100644 index badd999..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_8.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_8.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_8.imageset/classic_clubs_8.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_8.imageset/classic_clubs_8.png deleted file mode 100644 index bc59f18..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_8.imageset/classic_clubs_8.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_9.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_9.imageset/Contents.json deleted file mode 100644 index 8603075..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_9.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_9.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_9.imageset/classic_clubs_9.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_9.imageset/classic_clubs_9.png deleted file mode 100644 index 0b69ca1..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_9.imageset/classic_clubs_9.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_ace.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_ace.imageset/Contents.json deleted file mode 100644 index a3d1913..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_ace.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_ace.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_ace.imageset/classic_clubs_ace.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_ace.imageset/classic_clubs_ace.png deleted file mode 100644 index 09c52b6..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_ace.imageset/classic_clubs_ace.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_jack.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_jack.imageset/Contents.json deleted file mode 100644 index c257f8e..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_jack.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_jack.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_jack.imageset/classic_clubs_jack.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_jack.imageset/classic_clubs_jack.png deleted file mode 100644 index fbf5f7a..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_jack.imageset/classic_clubs_jack.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_king.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_king.imageset/Contents.json deleted file mode 100644 index 4bd227b..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_king.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_king.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_king.imageset/classic_clubs_king.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_king.imageset/classic_clubs_king.png deleted file mode 100644 index d4e8b14..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_king.imageset/classic_clubs_king.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_queen.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_clubs_queen.imageset/Contents.json deleted file mode 100644 index 5fa5b9d..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_clubs_queen.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_clubs_queen.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_clubs_queen.imageset/classic_clubs_queen.png b/SoliCards/Resources/Assets.xcassets/classic_clubs_queen.imageset/classic_clubs_queen.png deleted file mode 100644 index 71add12..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_clubs_queen.imageset/classic_clubs_queen.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_10.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_10.imageset/Contents.json deleted file mode 100644 index 58d268b..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_10.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_10.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_10.imageset/classic_diamonds_10.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_10.imageset/classic_diamonds_10.png deleted file mode 100644 index ad6b845..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_10.imageset/classic_diamonds_10.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_2.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_2.imageset/Contents.json deleted file mode 100644 index 483a518..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_2.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_2.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_2.imageset/classic_diamonds_2.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_2.imageset/classic_diamonds_2.png deleted file mode 100644 index 76460b3..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_2.imageset/classic_diamonds_2.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_3.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_3.imageset/Contents.json deleted file mode 100644 index 92a29df..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_3.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_3.imageset/classic_diamonds_3.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_3.imageset/classic_diamonds_3.png deleted file mode 100644 index 920a94b..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_3.imageset/classic_diamonds_3.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_4.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_4.imageset/Contents.json deleted file mode 100644 index 422e672..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_4.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_4.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_4.imageset/classic_diamonds_4.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_4.imageset/classic_diamonds_4.png deleted file mode 100644 index 6820735..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_4.imageset/classic_diamonds_4.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_5.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_5.imageset/Contents.json deleted file mode 100644 index 9e8e524..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_5.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_5.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_5.imageset/classic_diamonds_5.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_5.imageset/classic_diamonds_5.png deleted file mode 100644 index a390f82..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_5.imageset/classic_diamonds_5.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_6.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_6.imageset/Contents.json deleted file mode 100644 index c25936b..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_6.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_6.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_6.imageset/classic_diamonds_6.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_6.imageset/classic_diamonds_6.png deleted file mode 100644 index 0b8e87b..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_6.imageset/classic_diamonds_6.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_7.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_7.imageset/Contents.json deleted file mode 100644 index e2f861f..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_7.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_7.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_7.imageset/classic_diamonds_7.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_7.imageset/classic_diamonds_7.png deleted file mode 100644 index f35b880..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_7.imageset/classic_diamonds_7.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_8.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_8.imageset/Contents.json deleted file mode 100644 index 1670252..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_8.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_8.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_8.imageset/classic_diamonds_8.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_8.imageset/classic_diamonds_8.png deleted file mode 100644 index 268852b..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_8.imageset/classic_diamonds_8.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_9.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_9.imageset/Contents.json deleted file mode 100644 index b71b466..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_9.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_9.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_9.imageset/classic_diamonds_9.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_9.imageset/classic_diamonds_9.png deleted file mode 100644 index 8d7e789..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_9.imageset/classic_diamonds_9.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_ace.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_ace.imageset/Contents.json deleted file mode 100644 index f0ee3a2..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_ace.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_ace.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_ace.imageset/classic_diamonds_ace.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_ace.imageset/classic_diamonds_ace.png deleted file mode 100644 index 93b964a..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_ace.imageset/classic_diamonds_ace.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_jack.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_jack.imageset/Contents.json deleted file mode 100644 index 386ca05..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_jack.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_jack.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_jack.imageset/classic_diamonds_jack.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_jack.imageset/classic_diamonds_jack.png deleted file mode 100644 index 2fab1b9..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_jack.imageset/classic_diamonds_jack.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_king.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_king.imageset/Contents.json deleted file mode 100644 index 05b9848..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_king.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_king.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_king.imageset/classic_diamonds_king.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_king.imageset/classic_diamonds_king.png deleted file mode 100644 index fc930da..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_king.imageset/classic_diamonds_king.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_queen.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_diamonds_queen.imageset/Contents.json deleted file mode 100644 index 278cf59..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_diamonds_queen.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_diamonds_queen.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_diamonds_queen.imageset/classic_diamonds_queen.png b/SoliCards/Resources/Assets.xcassets/classic_diamonds_queen.imageset/classic_diamonds_queen.png deleted file mode 100644 index 7e2f86c..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_diamonds_queen.imageset/classic_diamonds_queen.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_10.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_10.imageset/Contents.json deleted file mode 100644 index e687cfe..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_10.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_10.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_10.imageset/classic_hearts_10.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_10.imageset/classic_hearts_10.png deleted file mode 100644 index bab6d78..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_10.imageset/classic_hearts_10.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_2.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_2.imageset/Contents.json deleted file mode 100644 index 45709b3..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_2.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_2.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_2.imageset/classic_hearts_2.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_2.imageset/classic_hearts_2.png deleted file mode 100644 index 982a61d..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_2.imageset/classic_hearts_2.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_3.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_3.imageset/Contents.json deleted file mode 100644 index 48a5606..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_3.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_3.imageset/classic_hearts_3.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_3.imageset/classic_hearts_3.png deleted file mode 100644 index e24f3d1..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_3.imageset/classic_hearts_3.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_4.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_4.imageset/Contents.json deleted file mode 100644 index 6f06b86..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_4.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_4.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_4.imageset/classic_hearts_4.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_4.imageset/classic_hearts_4.png deleted file mode 100644 index 2f6a7fe..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_4.imageset/classic_hearts_4.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_5.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_5.imageset/Contents.json deleted file mode 100644 index 535fc96..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_5.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_5.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_5.imageset/classic_hearts_5.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_5.imageset/classic_hearts_5.png deleted file mode 100644 index e103e2f..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_5.imageset/classic_hearts_5.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_6.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_6.imageset/Contents.json deleted file mode 100644 index 649145d..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_6.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_6.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_6.imageset/classic_hearts_6.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_6.imageset/classic_hearts_6.png deleted file mode 100644 index 5b6e416..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_6.imageset/classic_hearts_6.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_7.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_7.imageset/Contents.json deleted file mode 100644 index 0929ed0..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_7.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_7.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_7.imageset/classic_hearts_7.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_7.imageset/classic_hearts_7.png deleted file mode 100644 index 02a68e2..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_7.imageset/classic_hearts_7.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_8.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_8.imageset/Contents.json deleted file mode 100644 index 7c3e7f5..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_8.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_8.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_8.imageset/classic_hearts_8.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_8.imageset/classic_hearts_8.png deleted file mode 100644 index bb2ad6d..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_8.imageset/classic_hearts_8.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_9.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_9.imageset/Contents.json deleted file mode 100644 index e9cb789..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_9.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_9.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_9.imageset/classic_hearts_9.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_9.imageset/classic_hearts_9.png deleted file mode 100644 index 5011949..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_9.imageset/classic_hearts_9.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_ace.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_ace.imageset/Contents.json deleted file mode 100644 index 275009a..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_ace.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_ace.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_ace.imageset/classic_hearts_ace.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_ace.imageset/classic_hearts_ace.png deleted file mode 100644 index bd2f43a..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_ace.imageset/classic_hearts_ace.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_jack.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_jack.imageset/Contents.json deleted file mode 100644 index fb2eac2..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_jack.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_jack.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_jack.imageset/classic_hearts_jack.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_jack.imageset/classic_hearts_jack.png deleted file mode 100644 index e37a150..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_jack.imageset/classic_hearts_jack.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_king.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_king.imageset/Contents.json deleted file mode 100644 index 81b0412..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_king.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_king.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_king.imageset/classic_hearts_king.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_king.imageset/classic_hearts_king.png deleted file mode 100644 index 712bd18..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_king.imageset/classic_hearts_king.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_queen.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_hearts_queen.imageset/Contents.json deleted file mode 100644 index ee49b59..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_hearts_queen.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_hearts_queen.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_hearts_queen.imageset/classic_hearts_queen.png b/SoliCards/Resources/Assets.xcassets/classic_hearts_queen.imageset/classic_hearts_queen.png deleted file mode 100644 index 90e3e64..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_hearts_queen.imageset/classic_hearts_queen.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_10.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_10.imageset/Contents.json deleted file mode 100644 index 77cedc8..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_10.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_10.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_10.imageset/classic_spades_10.png b/SoliCards/Resources/Assets.xcassets/classic_spades_10.imageset/classic_spades_10.png deleted file mode 100644 index 6aa1536..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_10.imageset/classic_spades_10.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_2.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_2.imageset/Contents.json deleted file mode 100644 index a88f364..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_2.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_2.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_2.imageset/classic_spades_2.png b/SoliCards/Resources/Assets.xcassets/classic_spades_2.imageset/classic_spades_2.png deleted file mode 100644 index 3eb8a21..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_2.imageset/classic_spades_2.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_3.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_3.imageset/Contents.json deleted file mode 100644 index 4d6f8d1..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_3.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_3.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_3.imageset/classic_spades_3.png b/SoliCards/Resources/Assets.xcassets/classic_spades_3.imageset/classic_spades_3.png deleted file mode 100644 index e4b12e7..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_3.imageset/classic_spades_3.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_4.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_4.imageset/Contents.json deleted file mode 100644 index d769ccf..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_4.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_4.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_4.imageset/classic_spades_4.png b/SoliCards/Resources/Assets.xcassets/classic_spades_4.imageset/classic_spades_4.png deleted file mode 100644 index 30dcb16..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_4.imageset/classic_spades_4.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_5.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_5.imageset/Contents.json deleted file mode 100644 index 2c504b5..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_5.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_5.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_5.imageset/classic_spades_5.png b/SoliCards/Resources/Assets.xcassets/classic_spades_5.imageset/classic_spades_5.png deleted file mode 100644 index f1b0cdd..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_5.imageset/classic_spades_5.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_6.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_6.imageset/Contents.json deleted file mode 100644 index 81fe011..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_6.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_6.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_6.imageset/classic_spades_6.png b/SoliCards/Resources/Assets.xcassets/classic_spades_6.imageset/classic_spades_6.png deleted file mode 100644 index e508fc1..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_6.imageset/classic_spades_6.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_7.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_7.imageset/Contents.json deleted file mode 100644 index b1b93d8..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_7.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_7.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_7.imageset/classic_spades_7.png b/SoliCards/Resources/Assets.xcassets/classic_spades_7.imageset/classic_spades_7.png deleted file mode 100644 index 10e22a5..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_7.imageset/classic_spades_7.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_8.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_8.imageset/Contents.json deleted file mode 100644 index 63fcdfd..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_8.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_8.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_8.imageset/classic_spades_8.png b/SoliCards/Resources/Assets.xcassets/classic_spades_8.imageset/classic_spades_8.png deleted file mode 100644 index 65ab056..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_8.imageset/classic_spades_8.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_9.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_9.imageset/Contents.json deleted file mode 100644 index ee0e36f..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_9.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_9.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_9.imageset/classic_spades_9.png b/SoliCards/Resources/Assets.xcassets/classic_spades_9.imageset/classic_spades_9.png deleted file mode 100644 index 3371865..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_9.imageset/classic_spades_9.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_ace.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_ace.imageset/Contents.json deleted file mode 100644 index bff30e9..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_ace.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_ace.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_ace.imageset/classic_spades_ace.png b/SoliCards/Resources/Assets.xcassets/classic_spades_ace.imageset/classic_spades_ace.png deleted file mode 100644 index 23fbfdb..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_ace.imageset/classic_spades_ace.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_jack.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_jack.imageset/Contents.json deleted file mode 100644 index 639b790..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_jack.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_jack.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_jack.imageset/classic_spades_jack.png b/SoliCards/Resources/Assets.xcassets/classic_spades_jack.imageset/classic_spades_jack.png deleted file mode 100644 index 34785eb..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_jack.imageset/classic_spades_jack.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_king.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_king.imageset/Contents.json deleted file mode 100644 index 2b373b2..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_king.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_king.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_king.imageset/classic_spades_king.png b/SoliCards/Resources/Assets.xcassets/classic_spades_king.imageset/classic_spades_king.png deleted file mode 100644 index adda6c6..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_king.imageset/classic_spades_king.png and /dev/null differ diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_queen.imageset/Contents.json b/SoliCards/Resources/Assets.xcassets/classic_spades_queen.imageset/Contents.json deleted file mode 100644 index 5356cf8..0000000 --- a/SoliCards/Resources/Assets.xcassets/classic_spades_queen.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "classic_spades_queen.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SoliCards/Resources/Assets.xcassets/classic_spades_queen.imageset/classic_spades_queen.png b/SoliCards/Resources/Assets.xcassets/classic_spades_queen.imageset/classic_spades_queen.png deleted file mode 100644 index 208895d..0000000 Binary files a/SoliCards/Resources/Assets.xcassets/classic_spades_queen.imageset/classic_spades_queen.png and /dev/null differ diff --git a/SoliCards/Resources/Localizable.xcstrings b/SoliCards/Resources/Localizable.xcstrings deleted file mode 100644 index 927d74c..0000000 --- a/SoliCards/Resources/Localizable.xcstrings +++ /dev/null @@ -1,120 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "You Win!" : { - "comment" : "Victory overlay title" - }, - "New Game" : { - "comment" : "Button to start a new game" - }, - "Game" : { - "comment" : "Game menu label" - }, - "Undo" : { - "comment" : "Undo button label" - }, - "Hint" : { - "comment" : "Hint button label" - }, - "Auto" : { - "comment" : "Auto-complete button label" - }, - "Rules" : { - "comment" : "Rules button label" - }, - "Settings" : { - "comment" : "Settings button/title" - }, - "Stats" : { - "comment" : "Statistics button label" - }, - "Sound" : { - "comment" : "Sound toggle button label" - }, - "Theme" : { - "comment" : "Theme section title" - }, - "Card Style" : { - "comment" : "Card style section title" - }, - "Card Back" : { - "comment" : "Card back section title" - }, - "Sound Effects" : { - "comment" : "Sound effects toggle label" - }, - "Done" : { - "comment" : "Dismiss button" - }, - "Cancel" : { - "comment" : "Cancel button" - }, - "Start Game" : { - "comment" : "Start game button" - }, - "Variant" : { - "comment" : "Game variant picker label" - }, - "Difficulty" : { - "comment" : "Difficulty section/picker label" - }, - "Klondike" : { - "comment" : "Game variant name" - }, - "Spider" : { - "comment" : "Game variant name" - }, - "FreeCell" : { - "comment" : "Game variant name" - }, - "Easy" : { - "comment" : "Difficulty level" - }, - "Medium" : { - "comment" : "Difficulty level" - }, - "Hard" : { - "comment" : "Difficulty level" - }, - "Expert" : { - "comment" : "Difficulty level" - }, - "Objective" : { - "comment" : "Rules section title" - }, - "Tableau" : { - "comment" : "Rules section title" - }, - "Foundation" : { - "comment" : "Rules section title" - }, - "Stock & Waste" : { - "comment" : "Rules section title" - }, - "Free Cells" : { - "comment" : "Rules section title" - }, - "Power Moves" : { - "comment" : "Rules section title" - }, - "No Statistics Yet" : { - "comment" : "Empty state title for statistics" - }, - "Play some games to see your stats here." : { - "comment" : "Empty state description for statistics" - }, - "Statistics" : { - "comment" : "Statistics view title" - }, - "Empty card slot" : { - "comment" : "Accessibility label for empty card position" - }, - "Face down card" : { - "comment" : "Accessibility label for face-down card" - }, - "Double tap to move to best available position" : { - "comment" : "Accessibility hint for face-up cards" - } - }, - "version" : "1.0" -} diff --git a/SoliCards/Resources/PrivacyInfo.xcprivacy b/SoliCards/Resources/PrivacyInfo.xcprivacy deleted file mode 100644 index 5704bed..0000000 --- a/SoliCards/Resources/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,23 +0,0 @@ - - - - - NSPrivacyTracking - - NSPrivacyTrackingDomains - - NSPrivacyCollectedDataTypes - - NSPrivacyAccessedAPITypes - - - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults - NSPrivacyAccessedAPITypeReasons - - CA92.1 - - - - - diff --git a/SoliCards/Services/HapticManager.swift b/SoliCards/Services/HapticManager.swift deleted file mode 100644 index 49de5a6..0000000 --- a/SoliCards/Services/HapticManager.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -#if canImport(UIKit) -import UIKit -#endif - -@MainActor -enum HapticManager { - static func impact(_ style: HapticStyle = .medium) { - #if canImport(UIKit) && !os(macOS) - let generator: UIImpactFeedbackGenerator - switch style { - case .light: - generator = UIImpactFeedbackGenerator(style: .light) - case .medium: - generator = UIImpactFeedbackGenerator(style: .medium) - case .heavy: - generator = UIImpactFeedbackGenerator(style: .heavy) - } - generator.impactOccurred() - #endif - } - - static func notification(_ type: HapticNotification) { - #if canImport(UIKit) && !os(macOS) - let generator = UINotificationFeedbackGenerator() - switch type { - case .success: - generator.notificationOccurred(.success) - case .warning: - generator.notificationOccurred(.warning) - case .error: - generator.notificationOccurred(.error) - } - #endif - } -} - -enum HapticStyle: Sendable { - case light, medium, heavy -} - -enum HapticNotification: Sendable { - case success, warning, error -} diff --git a/SoliCards/Services/SoundManager.swift b/SoliCards/Services/SoundManager.swift deleted file mode 100644 index d9a7fc0..0000000 --- a/SoliCards/Services/SoundManager.swift +++ /dev/null @@ -1,46 +0,0 @@ -import AVFoundation -import Foundation - -enum SoundEffect: String, Sendable { - case cardFlip = "card_flip" - case cardPlace = "card_place" - case error = "error" - case victory = "victory" -} - -@Observable -final class SoundManager: @unchecked Sendable { - var isEnabled: Bool = true - private var players: [SoundEffect: AVAudioPlayer] = [:] - private let queue = DispatchQueue(label: "com.solicards.sound") - - init() { - preload() - } - - func play(_ effect: SoundEffect) async { - guard isEnabled else { return } - queue.async { [weak self] in - guard let player = self?.players[effect] else { return } - player.currentTime = 0 - player.play() - } - } - - private func preload() { - for effect in SoundEffect.allCases { - if let url = Bundle.main.url(forResource: effect.rawValue, withExtension: "caf", - subdirectory: "Sounds") { - do { - let player = try AVAudioPlayer(contentsOf: url) - player.prepareToPlay() - players[effect] = player - } catch { - // Sound will be unavailable — not a fatal error - } - } - } - } -} - -extension SoundEffect: CaseIterable {} diff --git a/SoliCards/Services/TimerService.swift b/SoliCards/Services/TimerService.swift deleted file mode 100644 index f8afef8..0000000 --- a/SoliCards/Services/TimerService.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import Observation - -@MainActor -@Observable -final class TimerService { - var elapsedSeconds: Int = 0 - private var task: Task? - private var isRunning = false - - func start() { - guard !isRunning else { return } - isRunning = true - task = Task { [weak self] in - while !Task.isCancelled { - try? await Task.sleep(for: .seconds(1)) - guard let self, self.isRunning else { break } - self.elapsedSeconds += 1 - } - } - } - - func stop() { - isRunning = false - task?.cancel() - task = nil - } - - func reset() { - stop() - elapsedSeconds = 0 - } - - var formattedTime: String { - let minutes = elapsedSeconds / 60 - let seconds = elapsedSeconds % 60 - return String(format: "%d:%02d", minutes, seconds) - } -} diff --git a/SoliCards/SoliCardsApp.swift b/SoliCards/SoliCardsApp.swift deleted file mode 100644 index a4d4a32..0000000 --- a/SoliCards/SoliCardsApp.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftUI -import SwiftData - -@main -struct SoliCardsApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - .modelContainer(for: [GameRecord.self, StatsRecord.self, PrefsRecord.self]) - } -} diff --git a/SoliCards/Theme/GameTheme.swift b/SoliCards/Theme/GameTheme.swift deleted file mode 100644 index 7dfc2cf..0000000 --- a/SoliCards/Theme/GameTheme.swift +++ /dev/null @@ -1,73 +0,0 @@ -import SwiftUI - -struct GameTheme: Identifiable, Equatable, Sendable { - let id: String - let displayName: String - let backgroundColor: Color - let tableauColor: Color - let accentColor: Color - let cardTintColor: Color - - static let allThemes: [GameTheme] = [ - .classicGreen, - .darkMode, - .oceanBlue, - .royalPurple, - .forestGreen, - .sunsetOrange, - ] - - static let classicGreen = GameTheme( - id: "classic", - displayName: "Classic Green", - backgroundColor: Color(red: 0.0, green: 0.5, blue: 0.0), - tableauColor: Color(red: 0.0, green: 0.4, blue: 0.0), - accentColor: Color.white, - cardTintColor: Color(red: 0.0, green: 0.6, blue: 0.0) - ) - - static let darkMode = GameTheme( - id: "dark", - displayName: "Dark Mode", - backgroundColor: Color(red: 0.12, green: 0.12, blue: 0.14), - tableauColor: Color(red: 0.18, green: 0.18, blue: 0.2), - accentColor: Color(red: 0.4, green: 0.6, blue: 1.0), - cardTintColor: Color(red: 0.25, green: 0.25, blue: 0.28) - ) - - static let oceanBlue = GameTheme( - id: "ocean", - displayName: "Ocean Blue", - backgroundColor: Color(red: 0.1, green: 0.3, blue: 0.5), - tableauColor: Color(red: 0.08, green: 0.25, blue: 0.42), - accentColor: Color(red: 0.6, green: 0.85, blue: 1.0), - cardTintColor: Color(red: 0.15, green: 0.35, blue: 0.55) - ) - - static let royalPurple = GameTheme( - id: "purple", - displayName: "Royal Purple", - backgroundColor: Color(red: 0.3, green: 0.15, blue: 0.45), - tableauColor: Color(red: 0.25, green: 0.12, blue: 0.38), - accentColor: Color(red: 0.85, green: 0.7, blue: 1.0), - cardTintColor: Color(red: 0.35, green: 0.2, blue: 0.5) - ) - - static let forestGreen = GameTheme( - id: "forest", - displayName: "Forest Green", - backgroundColor: Color(red: 0.1, green: 0.35, blue: 0.15), - tableauColor: Color(red: 0.08, green: 0.28, blue: 0.12), - accentColor: Color(red: 0.6, green: 0.9, blue: 0.65), - cardTintColor: Color(red: 0.15, green: 0.4, blue: 0.2) - ) - - static let sunsetOrange = GameTheme( - id: "sunset", - displayName: "Sunset Orange", - backgroundColor: Color(red: 0.6, green: 0.25, blue: 0.1), - tableauColor: Color(red: 0.5, green: 0.2, blue: 0.08), - accentColor: Color(red: 1.0, green: 0.85, blue: 0.5), - cardTintColor: Color(red: 0.65, green: 0.3, blue: 0.15) - ) -} diff --git a/SoliCards/Theme/ThemeManager.swift b/SoliCards/Theme/ThemeManager.swift deleted file mode 100644 index 524dd66..0000000 --- a/SoliCards/Theme/ThemeManager.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import Observation - -@Observable -final class ThemeManager { - var currentTheme: GameTheme = .classicGreen - - func applyTheme(_ theme: GameTheme) { - currentTheme = theme - } -} diff --git a/SoliCards/ViewModels/GameViewModel.swift b/SoliCards/ViewModels/GameViewModel.swift deleted file mode 100644 index 91359ce..0000000 --- a/SoliCards/ViewModels/GameViewModel.swift +++ /dev/null @@ -1,372 +0,0 @@ -import Foundation -import Observation -import SwiftUI - -@MainActor -@Observable -final class GameViewModel { - // MARK: - Dependencies - - private(set) var rules: GameRules - private let soundManager = SoundManager() - private let timerService = TimerService() - - // MARK: - Persistence - - var persistenceManager: PersistenceManager? - private var autoSaveTask: Task? - - // MARK: - State - - private(set) var state = GameState() - private(set) var variant: GameVariant - var difficulty: Difficulty - - // MARK: - Drag state - - var draggedCards: [Card] = [] - var dragSource: CardLocation? - var dragPosition: CGPoint = .zero - var dropTargets: [DropTargetData] = [] - - // MARK: - Hint state - - var currentHint: HintResult? - var isShowingHint: Bool = false - - // MARK: - Timer - - var elapsedSeconds: Int { timerService.elapsedSeconds } - - // MARK: - Computed - - var canUndo: Bool { - state.canUndo && undosRemaining > 0 - } - - var isWon: Bool { state.phase == .won } - - private var undosRemaining: Int - - // MARK: - Init - - init(variant: GameVariant = .klondike, difficulty: Difficulty = .medium) { - self.variant = variant - self.difficulty = difficulty - self.rules = GameRulesFactory.rules(for: variant) - self.undosRemaining = difficulty.settings.maxUndos - } - - // MARK: - Game Actions - - func newGame() { - // Record loss for previous game if it was in progress - if state.phase == .playing { - persistenceManager?.recordLoss(variant: variant, difficulty: difficulty) - } - - rules = GameRulesFactory.rules(for: variant) - let deck = variant.deckCount == 2 ? Deck.double() : Deck.standard() - let snapshot = rules.deal(deck: deck) - state.reset(from: snapshot, variant: variant) - undosRemaining = difficulty.settings.maxUndos - timerService.reset() - timerService.start() - playSound(.cardFlip) - scheduleAutoSave() - } - - func changeVariant(to newVariant: GameVariant) { - variant = newVariant - newGame() - } - - /// Resume a saved game from a GameRecord. - func resumeGame(from record: GameRecord) { - guard let snapshot = record.decodedSnapshot, - let savedVariant = record.gameVariant, - let savedDifficulty = record.gameDifficulty else { return } - - variant = savedVariant - difficulty = savedDifficulty - rules = GameRulesFactory.rules(for: variant) - - state.restore(from: snapshot) - state.phase = .playing - if variant.hasFreeCells && state.freeCells.isEmpty { - state.freeCells = Array(repeating: nil, count: variant.freeCellCount) - } - - undosRemaining = difficulty.settings.maxUndos - timerService.reset() - timerService.elapsedSeconds = record.elapsedSeconds - timerService.start() - } - - func tapCard(at location: CardLocation, cardIndex: Int) { - guard state.phase == .playing else { return } - - switch location { - case .stock: - drawFromStock() - case .waste: - guard let card = state.waste.last else { return } - if let dest = findBestDestination(for: [card], from: .waste) { - executeMove(cards: [card], from: .waste, to: dest) - } - case .tableau(let tabIndex): - let tableau = state.tableaus[tabIndex] - guard cardIndex == tableau.count - 1, let card = tableau.last, card.isFaceUp else { return } - if let dest = findBestDestination(for: [card], from: location) { - executeMove(cards: [card], from: location, to: dest) - } - case .freeCell(let cellIndex): - guard let card = state.freeCells[cellIndex] else { return } - if let dest = findBestDestination(for: [card], from: location) { - executeMove(cards: [card], from: location, to: dest) - } - case .foundation: - break - } - } - - func beginDrag(cards: [Card], from: CardLocation) { - guard state.phase == .playing else { return } - guard rules.canPickUp(cards: cards, from: from, state: state.snapshot()) else { return } - draggedCards = cards - dragSource = from - } - - func drop(at destination: CardLocation) -> Bool { - guard let source = dragSource, !draggedCards.isEmpty else { - cancelDrag() - return false - } - - let snapshot = state.snapshot() - guard rules.canMove(cards: draggedCards, from: source, to: destination, state: snapshot) else { - cancelDrag() - return false - } - - executeMove(cards: draggedCards, from: source, to: destination) - clearDrag() - return true - } - - func cancelDrag() { - clearDrag() - } - - /// Attempt to drop at the given point in the board coordinate space. - /// Falls back to cancel if no valid target found. - func endDrag(at point: CGPoint) { - guard !draggedCards.isEmpty else { - clearDrag() - return - } - - // Find the drop target under the finger - let candidates = dropTargets.filter { $0.frame.contains(point) } - let target = candidates.min(by: { - $0.frame.width * $0.frame.height < $1.frame.width * $1.frame.height - }) - - if let target { - _ = drop(at: target.location) - } else { - cancelDrag() - } - } - - func undo() { - guard canUndo, let snapshot = state.popHistory() else { return } - state.restore(from: snapshot) - undosRemaining -= 1 - playSound(.cardFlip) - scheduleAutoSave() - } - - func requestHint() { - let hints = rules.findHints(state: state.snapshot(), settings: difficulty.settings) - currentHint = hints.first - if currentHint != nil { - isShowingHint = true - } - } - - func drawFromStock() { - guard state.phase == .playing else { return } - state.pushHistory() - var snapshot = state.snapshot() - if rules.drawFromStock(state: &snapshot, drawCount: difficulty.settings.drawCount) != nil { - state.restore(from: snapshot) - state.moves += 1 - playSound(.cardFlip) - scheduleAutoSave() - } - } - - func autoComplete() { - guard state.phase == .playing else { return } - guard rules.canAutoComplete(state: state.snapshot()) else { return } - state.phase = .autoCompleting - - Task { - while state.phase == .autoCompleting { - guard let (from, to) = AutoCompleter.findNextAutoMove(state: state.snapshot(), rules: rules) else { - break - } - guard let card = cardAt(location: from) else { break } - executeMove(cards: [card], from: from, to: to) - try? await Task.sleep(for: .milliseconds(150)) - } - - if rules.isWon(state: state.snapshot()) { - handleWin() - } else { - state.phase = .playing - } - } - } - - func pause() { - guard state.phase == .playing else { return } - state.phase = .paused - timerService.stop() - saveGameNow() - } - - func resume() { - guard state.phase == .paused else { return } - state.phase = .playing - timerService.start() - } - - /// Save game state immediately (called on app background/close). - func saveGameNow() { - guard state.phase == .playing || state.phase == .paused else { return } - persistenceManager?.saveGame( - variant: variant, - difficulty: difficulty, - snapshot: state.snapshot(), - elapsedSeconds: timerService.elapsedSeconds - ) - } - - var isSoundEnabled: Bool { - get { soundManager.isEnabled } - set { soundManager.isEnabled = newValue } - } - - // MARK: - Private - - private func cardAt(location: CardLocation) -> Card? { - switch location { - case .tableau(let i): state.tableaus[i].last - case .waste: state.waste.last - case .freeCell(let i): state.freeCells[i] - case .foundation(let i): state.foundations[i].last - case .stock: state.stock.last - } - } - - private func executeMove(cards: [Card], from: CardLocation, to: CardLocation) { - state.pushHistory() - - // Remove cards from source - switch from { - case .tableau(let index): - state.tableaus[index].removeLast(cards.count) - if let lastIndex = state.tableaus[index].indices.last, - !state.tableaus[index][lastIndex].isFaceUp { - state.tableaus[index][lastIndex].isFaceUp = true - } - case .waste: - state.waste.removeLast() - case .freeCell(let index): - state.freeCells[index] = nil - case .foundation(let index): - state.foundations[index].removeLast() - case .stock: - break - } - - // Add cards to destination - switch to { - case .tableau(let index): - state.tableaus[index].append(contentsOf: cards) - case .foundation(let index): - state.foundations[index].append(contentsOf: cards) - case .freeCell(let index): - state.freeCells[index] = cards.first - case .waste: - state.waste.append(contentsOf: cards) - case .stock: - break - } - - state.moves += 1 - state.score += rules.scoreForMove(from: from, to: to) - - // Check for Spider complete sequences - if variant == .spider, let spiderRules = rules as? SpiderRules { - var snapshot = state.snapshot() - if spiderRules.checkAndMoveCompleteSequences(state: &snapshot) { - state.restore(from: snapshot) - playSound(.cardPlace) - } - } - - // Check win - if rules.isWon(state: state.snapshot()) { - handleWin() - } else { - playSound(.cardPlace) - scheduleAutoSave() - } - } - - private func handleWin() { - state.phase = .won - timerService.stop() - playSound(.victory) - - // Record win in statistics - persistenceManager?.recordWin( - variant: variant, - difficulty: difficulty, - score: state.score, - time: timerService.elapsedSeconds - ) - - // Delete saved game (game is over) - persistenceManager?.deleteSavedGame(for: variant) - } - - /// Debounced auto-save: waits 2 seconds of inactivity before saving. - private func scheduleAutoSave() { - autoSaveTask?.cancel() - autoSaveTask = Task { - try? await Task.sleep(for: .seconds(2)) - guard !Task.isCancelled else { return } - saveGameNow() - } - } - - private func findBestDestination(for cards: [Card], from: CardLocation) -> CardLocation? { - let destinations = rules.validDestinations(for: cards, from: from, state: state.snapshot()) - return destinations.first { if case .foundation = $0 { return true }; return false } - ?? destinations.first - } - - private func clearDrag() { - draggedCards = [] - dragSource = nil - dragPosition = .zero - } - - private nonisolated func playSound(_ effect: SoundEffect) { - Task { await soundManager.play(effect) } - } -} diff --git a/SoliCards/ViewModels/SettingsViewModel.swift b/SoliCards/ViewModels/SettingsViewModel.swift deleted file mode 100644 index c03182a..0000000 --- a/SoliCards/ViewModels/SettingsViewModel.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import Observation - -@Observable -final class SettingsViewModel { - var theme: GameTheme = .classicGreen - var cardFaceStyle: CardFaceStyle = .classic - var cardBackDesign: CardBackDesign = .blue - var soundEnabled: Bool = true - var difficulty: Difficulty = .medium -} diff --git a/SoliCards/ViewModels/StatsViewModel.swift b/SoliCards/ViewModels/StatsViewModel.swift deleted file mode 100644 index 1484247..0000000 --- a/SoliCards/ViewModels/StatsViewModel.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import Observation -import SwiftData - -@Observable -final class StatsViewModel { - var records: [StatsRecord] = [] - - func loadStats(context: ModelContext) { - let descriptor = FetchDescriptor() - records = (try? context.fetch(descriptor)) ?? [] - } - - func stats(for variant: GameVariant, difficulty: Difficulty) -> StatsRecord? { - records.first { $0.variant == variant.rawValue && $0.difficulty == difficulty.rawValue } - } -} diff --git a/SoliCards/Views/Game/CardStackView.swift b/SoliCards/Views/Game/CardStackView.swift deleted file mode 100644 index c84244d..0000000 --- a/SoliCards/Views/Game/CardStackView.swift +++ /dev/null @@ -1,101 +0,0 @@ -import SwiftUI - -struct CardStackView: View { - let cards: [Card] - let location: CardLocation - let layout: CardLayout - let cardFaceStyle: CardFaceStyle - let cardBackDesign: CardBackDesign - @Bindable var viewModel: GameViewModel - - var body: some View { - ZStack(alignment: .top) { - // Empty slot placeholder — also a drop target - CardView(card: nil, cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign, - size: layout.cardSize()) - .dropTarget(location) - .accessibilityLabel(emptySlotLabel) - - // Stacked cards - ForEach(Array(cards.enumerated()), id: \.element.id) { index, card in - let isInDrag = viewModel.draggedCards.contains(card) - let isLast = index == cards.count - 1 - - CardView(card: card, cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign, - size: layout.cardSize(), - isHighlighted: isHinted(card)) - .offset(y: yOffset(for: index)) - .opacity(isInDrag ? 0.3 : 1.0) - .onTapGesture { - viewModel.tapCard(at: location, cardIndex: index) - } - .simultaneousGesture( - card.isFaceUp ? dragGesture(for: card, at: index) : nil - ) - .overlay { - if isLast { - Color.clear - .dropTarget(location) - .offset(y: yOffset(for: index)) - } - } - } - } - .frame(width: layout.cardWidth) - .accessibilityElement(children: .contain) - .accessibilityLabel(stackAccessibilityLabel) - } - - private var emptySlotLabel: Text { - switch location { - case .tableau(let i): Text("Empty tableau column \(i + 1)") - case .foundation(let i): Text("Empty foundation \(i + 1)") - case .freeCell(let i): Text("Empty free cell \(i + 1)") - default: Text("Empty slot") - } - } - - private var stackAccessibilityLabel: Text { - switch location { - case .tableau(let i): - let count = cards.count - return Text("Tableau column \(i + 1), \(count) card\(count == 1 ? "" : "s")") - default: - return Text("") - } - } - - private func yOffset(for index: Int) -> CGFloat { - var offset: CGFloat = 0 - for i in 0.. some Gesture { - LongPressGesture(minimumDuration: 0.15) - .sequenced(before: DragGesture(coordinateSpace: .named("board"))) - .onChanged { value in - if case .second(true, let drag?) = value { - if viewModel.draggedCards.isEmpty { - let cardsToMove = Array(cards[index...]) - viewModel.beginDrag(cards: cardsToMove, from: location) - } - viewModel.dragPosition = drag.location - } - } - .onEnded { value in - if case .second(true, let drag?) = value { - viewModel.endDrag(at: drag.location) - } else { - viewModel.cancelDrag() - } - } - } - - private func isHinted(_ card: Card) -> Bool { - guard viewModel.isShowingHint, let hint = viewModel.currentHint else { return false } - return hint.cards.contains(card) - } -} diff --git a/SoliCards/Views/Game/CardView.swift b/SoliCards/Views/Game/CardView.swift deleted file mode 100644 index 215b119..0000000 --- a/SoliCards/Views/Game/CardView.swift +++ /dev/null @@ -1,123 +0,0 @@ -import SwiftUI - -struct CardView: View { - let card: Card? - let cardFaceStyle: CardFaceStyle - let cardBackDesign: CardBackDesign - let size: CGSize - var isHighlighted: Bool = false - - @Environment(\.accessibilityReduceMotion) private var reduceMotion - - var body: some View { - Group { - if let card { - if card.isFaceUp { - cardFront(card) - } else { - cardBack - } - } else { - emptySlot - } - } - .frame(width: size.width, height: size.height) - .clipShape(RoundedRectangle(cornerRadius: size.width * 0.08)) - .shadow(color: .black.opacity(0.15), radius: 2, y: 1) - .overlay { - if isHighlighted { - RoundedRectangle(cornerRadius: size.width * 0.08) - .stroke(Color.yellow, lineWidth: 3) - .animation(reduceMotion ? nil : .easeInOut(duration: 0.8).repeatForever(), value: isHighlighted) - } - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(accessibilityLabel) - .accessibilityHint(accessibilityHint) - .accessibilityAddTraits(card?.isFaceUp == true ? .isButton : []) - } - - private var accessibilityLabel: Text { - guard let card else { return Text("Empty card slot") } - if card.isFaceUp { - return Text("\(card.rank.displayName) of \(card.suit.displayName)") - } else { - return Text("Face down card") - } - } - - private var accessibilityHint: Text { - guard let card, card.isFaceUp else { return Text("") } - return Text("Double tap to move to best available position") - } - - private func cardFront(_ card: Card) -> some View { - let imageName = card.frontImageName(style: cardFaceStyle) - - return ZStack { - RoundedRectangle(cornerRadius: size.width * 0.08) - .fill(.white) - - if let uiImage = loadImage(named: imageName) { - Image(uiImage: uiImage) - .resizable() - .aspectRatio(contentMode: .fit) - .padding(1) - .accessibilityHidden(true) - } else { - VStack(spacing: 2) { - Text(card.rank.shortName) - .font(.system(size: size.width * 0.28, weight: .bold)) - Text(card.suit.symbol) - .font(.system(size: size.width * 0.22)) - } - .foregroundStyle(card.color == .red ? .red : .black) - .accessibilityHidden(true) - } - } - } - - private var cardBack: some View { - let imageName = cardBackDesign.imageName - - return ZStack { - RoundedRectangle(cornerRadius: size.width * 0.08) - .fill(Color.blue) - - if let uiImage = loadImage(named: imageName) { - Image(uiImage: uiImage) - .resizable() - .aspectRatio(contentMode: .fit) - .padding(2) - .accessibilityHidden(true) - } else { - RoundedRectangle(cornerRadius: size.width * 0.08) - .strokeBorder(Color.white.opacity(0.3), lineWidth: 2) - .padding(3) - } - } - } - - private var emptySlot: some View { - RoundedRectangle(cornerRadius: size.width * 0.08) - .strokeBorder(Color.white.opacity(0.3), lineWidth: 2) - } - - #if os(macOS) - private func loadImage(named name: String) -> NSImage? { - NSImage(named: name) - } - - private func Image(uiImage: NSImage) -> SwiftUI.Image { - SwiftUI.Image(nsImage: uiImage) - } - #else - private func loadImage(named name: String) -> UIImage? { - UIImage(named: name) - } - - private func Image(uiImage: UIImage) -> SwiftUI.Image { - SwiftUI.Image(uiImage: uiImage) - } - #endif -} diff --git a/SoliCards/Views/Game/DraggedCardsOverlay.swift b/SoliCards/Views/Game/DraggedCardsOverlay.swift deleted file mode 100644 index 57b4d96..0000000 --- a/SoliCards/Views/Game/DraggedCardsOverlay.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftUI - -struct DraggedCardsOverlay: View { - @Bindable var viewModel: GameViewModel - let layout: CardLayout - let cardFaceStyle: CardFaceStyle - let cardBackDesign: CardBackDesign - - var body: some View { - if !viewModel.draggedCards.isEmpty { - ZStack(alignment: .top) { - ForEach(Array(viewModel.draggedCards.enumerated()), id: \.element.id) { index, card in - CardView(card: card, cardFaceStyle: cardFaceStyle, - cardBackDesign: cardBackDesign, size: layout.cardSize()) - .offset(y: CGFloat(index) * layout.verticalOverlapFaceUp) - } - } - .frame(width: layout.cardWidth) - .position(viewModel.dragPosition) - .allowsHitTesting(false) - } - } -} diff --git a/SoliCards/Views/Game/DropTargetPreferenceKey.swift b/SoliCards/Views/Game/DropTargetPreferenceKey.swift deleted file mode 100644 index 474b7e0..0000000 --- a/SoliCards/Views/Game/DropTargetPreferenceKey.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftUI - -struct DropTargetData: Equatable, Sendable { - let location: CardLocation - let frame: CGRect -} - -struct DropTargetPreferenceKey: PreferenceKey { - nonisolated(unsafe) static var defaultValue: [DropTargetData] = [] - - static func reduce(value: inout [DropTargetData], nextValue: () -> [DropTargetData]) { - value.append(contentsOf: nextValue()) - } -} - -extension View { - func dropTarget(_ location: CardLocation) -> some View { - background( - GeometryReader { geometry in - Color.clear.preference( - key: DropTargetPreferenceKey.self, - value: [DropTargetData(location: location, - frame: geometry.frame(in: .named("board")))] - ) - } - ) - } -} diff --git a/SoliCards/Views/Game/FreeCellBoardView.swift b/SoliCards/Views/Game/FreeCellBoardView.swift deleted file mode 100644 index 9e29191..0000000 --- a/SoliCards/Views/Game/FreeCellBoardView.swift +++ /dev/null @@ -1,83 +0,0 @@ -import SwiftUI - -struct FreeCellBoardView: View { - @Bindable var viewModel: GameViewModel - let layout: CardLayout - let theme: GameTheme - let cardFaceStyle: CardFaceStyle - let cardBackDesign: CardBackDesign - - var body: some View { - VStack(spacing: layout.horizontalPadding) { - HStack(spacing: layout.horizontalPadding) { - ForEach(0.. some View { - let card = viewModel.state.freeCells[index] - return CardView(card: card, - cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign, - size: layout.cardSize()) - .dropTarget(.freeCell(index)) - .accessibilityLabel(card != nil - ? Text("Free cell \(index + 1), \(card!.rank.displayName) of \(card!.suit.displayName)") - : Text("Free cell \(index + 1), empty")) - .onTapGesture { viewModel.tapCard(at: .freeCell(index), cardIndex: 0) } - .simultaneousGesture(freeCellDragGesture(index: index)) - } - - private func foundationView(index: Int) -> some View { - let topCard = viewModel.state.foundations[index].last - return CardView(card: topCard, - cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign, - size: layout.cardSize()) - .dropTarget(.foundation(index)) - .accessibilityLabel(topCard != nil - ? Text("Foundation \(index + 1), \(topCard!.rank.displayName) of \(topCard!.suit.displayName)") - : Text("Foundation \(index + 1), empty")) - } - - private func freeCellDragGesture(index: Int) -> some Gesture { - LongPressGesture(minimumDuration: 0.15) - .sequenced(before: DragGesture(coordinateSpace: .named("board"))) - .onChanged { value in - if case .second(true, let drag?) = value { - if viewModel.draggedCards.isEmpty, let card = viewModel.state.freeCells[index] { - viewModel.beginDrag(cards: [card], from: .freeCell(index)) - } - viewModel.dragPosition = drag.location - } - } - .onEnded { value in - if case .second(true, let drag?) = value { - viewModel.endDrag(at: drag.location) - } else { - viewModel.cancelDrag() - } - } - } -} diff --git a/SoliCards/Views/Game/GameBoardView.swift b/SoliCards/Views/Game/GameBoardView.swift deleted file mode 100644 index 1ccc208..0000000 --- a/SoliCards/Views/Game/GameBoardView.swift +++ /dev/null @@ -1,116 +0,0 @@ -import SwiftUI - -struct GameBoardView: View { - @Bindable var viewModel: GameViewModel - let theme: GameTheme - let cardFaceStyle: CardFaceStyle - let cardBackDesign: CardBackDesign - - private var isReady: Bool { - viewModel.state.phase != .notStarted && !viewModel.state.tableaus.isEmpty - } - - private var deepestColumn: (faceDown: Int, faceUp: Int) { - var worst = (faceDown: 0, faceUp: 1) - for column in viewModel.state.tableaus { - let faceUp = column.reversed().prefix(while: { $0.isFaceUp }).count - let faceDown = column.count - faceUp - let depth = faceDown * 15 + max(0, faceUp - 1) * 25 + 100 - let worstDepth = worst.faceDown * 15 + max(0, worst.faceUp - 1) * 25 + 100 - if depth > worstDepth { - worst = (faceDown, faceUp) - } - } - return worst - } - - var body: some View { - if isReady { - VStack(spacing: 0) { - ScoreBarView( - moves: viewModel.state.moves, - score: viewModel.state.score, - time: viewModel.elapsedSeconds.formattedTime, - theme: theme - ) - - GeometryReader { geometry in - let layout = CardLayout( - availableSize: geometry.size, - variant: viewModel.variant, - deepestColumn: deepestColumn - ) - - ScrollView(.vertical) { - boardContent(layout: layout) - .padding(.bottom, layout.horizontalPadding) - .frame(maxWidth: .infinity, - minHeight: landscapeMinHeight(viewportHeight: geometry.size.height, - isLandscape: layout.isLandscape), - alignment: .top) - .contentShape(Rectangle()) - } - .scrollIndicators(.hidden) - .scrollDisabled(!viewModel.draggedCards.isEmpty) - .coordinateSpace(name: "board") - .onPreferenceChange(DropTargetPreferenceKey.self) { targets in - viewModel.dropTargets = targets - } - .overlay { - DraggedCardsOverlay( - viewModel: viewModel, - layout: layout, - cardFaceStyle: cardFaceStyle, - cardBackDesign: cardBackDesign - ) - } - } - } - .background(theme.backgroundColor) - .overlay { - if viewModel.isWon { - VictoryOverlayView { - viewModel.newGame() - } - } - } - } else { - Color.clear - .background(theme.backgroundColor) - } - } - - private func landscapeMinHeight(viewportHeight: CGFloat, isLandscape: Bool) -> CGFloat? { - #if os(iOS) - return isLandscape ? viewportHeight * 1.3 : nil - #else - return nil - #endif - } - - @ViewBuilder - private func boardContent(layout: CardLayout) -> some View { - switch viewModel.variant { - case .klondike: - KlondikeBoardView(viewModel: viewModel, layout: layout, - theme: theme, cardFaceStyle: cardFaceStyle, - cardBackDesign: cardBackDesign) - case .spider: - SpiderBoardView(viewModel: viewModel, layout: layout, - theme: theme, cardFaceStyle: cardFaceStyle, - cardBackDesign: cardBackDesign) - case .freeCell: - FreeCellBoardView(viewModel: viewModel, layout: layout, - theme: theme, cardFaceStyle: cardFaceStyle, - cardBackDesign: cardBackDesign) - } - } -} - -extension Int { - var formattedTime: String { - let minutes = self / 60 - let seconds = self % 60 - return String(format: "%d:%02d", minutes, seconds) - } -} diff --git a/SoliCards/Views/Game/KlondikeBoardView.swift b/SoliCards/Views/Game/KlondikeBoardView.swift deleted file mode 100644 index d4edbee..0000000 --- a/SoliCards/Views/Game/KlondikeBoardView.swift +++ /dev/null @@ -1,109 +0,0 @@ -import SwiftUI - -struct KlondikeBoardView: View { - @Bindable var viewModel: GameViewModel - let layout: CardLayout - let theme: GameTheme - let cardFaceStyle: CardFaceStyle - let cardBackDesign: CardBackDesign - - var body: some View { - VStack(spacing: layout.horizontalPadding) { - HStack(spacing: layout.horizontalPadding) { - stockView - wasteView - - // Empty gap at column 2 position - Color.clear - .frame(width: layout.cardWidth, height: layout.cardHeight) - - ForEach(0.. some View { - let topCard = viewModel.state.foundations[index].last - return CardView(card: topCard, - cardFaceStyle: cardFaceStyle, cardBackDesign: cardBackDesign, - size: layout.cardSize()) - .dropTarget(.foundation(index)) - .accessibilityLabel(topCard != nil - ? Text("Foundation \(index + 1), \(topCard!.rank.displayName) of \(topCard!.suit.displayName)") - : Text("Foundation \(index + 1), empty")) - } - - private var wasteDragGesture: some Gesture { - LongPressGesture(minimumDuration: 0.15) - .sequenced(before: DragGesture(coordinateSpace: .named("board"))) - .onChanged { value in - if case .second(true, let drag?) = value { - if viewModel.draggedCards.isEmpty, let card = viewModel.state.waste.last { - viewModel.beginDrag(cards: [card], from: .waste) - } - viewModel.dragPosition = drag.location - } - } - .onEnded { value in - if case .second(true, let drag?) = value { - viewModel.endDrag(at: drag.location) - } else { - viewModel.cancelDrag() - } - } - } -} diff --git a/SoliCards/Views/Game/ScoreBarView.swift b/SoliCards/Views/Game/ScoreBarView.swift deleted file mode 100644 index 598a909..0000000 --- a/SoliCards/Views/Game/ScoreBarView.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftUI - -struct ScoreBarView: View { - let moves: Int - let score: Int - let time: String - let theme: GameTheme - - var body: some View { - HStack { - Label("\(moves)", systemImage: "arrow.left.arrow.right") - .accessibilityElement(children: .ignore) - .accessibilityLabel(Text("\(moves) moves")) - Spacer() - Label("\(score)", systemImage: "star.fill") - .accessibilityElement(children: .ignore) - .accessibilityLabel(Text("Score \(score)")) - Spacer() - Label(time, systemImage: "clock") - .accessibilityElement(children: .ignore) - .accessibilityLabel(Text("Time \(time)")) - } - .font(.subheadline.monospacedDigit()) - .foregroundStyle(theme.accentColor) - .padding(.horizontal) - .padding(.vertical, 8) - .accessibilityElement(children: .contain) - .accessibilityLabel(Text("Game status")) - } -} diff --git a/SoliCards/Views/Game/SpiderBoardView.swift b/SoliCards/Views/Game/SpiderBoardView.swift deleted file mode 100644 index cde8540..0000000 --- a/SoliCards/Views/Game/SpiderBoardView.swift +++ /dev/null @@ -1,76 +0,0 @@ -import SwiftUI - -struct SpiderBoardView: View { - @Bindable var viewModel: GameViewModel - let layout: CardLayout - let theme: GameTheme - let cardFaceStyle: CardFaceStyle - let cardBackDesign: CardBackDesign - - var body: some View { - VStack(spacing: layout.horizontalPadding) { - HStack(alignment: .top, spacing: layout.horizontalPadding) { - ForEach(0.. 0 else { return 0 } - return (remaining + 9) / 10 - } -} diff --git a/SoliCards/Views/Game/VictoryOverlayView.swift b/SoliCards/Views/Game/VictoryOverlayView.swift deleted file mode 100644 index 6c9bba6..0000000 --- a/SoliCards/Views/Game/VictoryOverlayView.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftUI - -struct VictoryOverlayView: View { - let onNewGame: () -> Void - - @State private var showContent = false - @Environment(\.accessibilityReduceMotion) private var reduceMotion - - var body: some View { - ZStack { - Color.black.opacity(0.6) - .ignoresSafeArea() - - VStack(spacing: 24) { - Text("You Win!") - .font(.largeTitle.bold()) - .foregroundStyle(.white) - - Button("New Game") { - onNewGame() - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - } - .scaleEffect(showContent ? 1.0 : 0.5) - .opacity(showContent ? 1.0 : 0) - } - .onAppear { - if reduceMotion { - showContent = true - } else { - withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { - showContent = true - } - } - } - .accessibilityAddTraits(.isModal) - .accessibilityLabel(Text("You win! Double tap New Game to start a new game.")) - } -} diff --git a/SoliCards/Views/Menu/MainMenuView.swift b/SoliCards/Views/Menu/MainMenuView.swift deleted file mode 100644 index 0d2a853..0000000 --- a/SoliCards/Views/Menu/MainMenuView.swift +++ /dev/null @@ -1,59 +0,0 @@ -import SwiftUI - -struct MainMenuView: View { - @Binding var selectedVariant: GameVariant - let onStart: () -> Void - - var body: some View { - VStack(spacing: 32) { - Text("SoliCards") - .font(.largeTitle.bold()) - - VStack(spacing: 16) { - ForEach(GameVariant.allCases) { variant in - Button { - selectedVariant = variant - onStart() - } label: { - HStack { - Image(systemName: iconName(for: variant)) - .font(.title2) - .frame(width: 30) - VStack(alignment: .leading) { - Text(variant.displayName) - .font(.headline) - Text(subtitle(for: variant)) - .font(.caption) - .foregroundStyle(.secondary) - } - Spacer() - Image(systemName: "chevron.right") - .foregroundStyle(.tertiary) - } - .padding() - .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) - } - .buttonStyle(.plain) - } - } - .frame(maxWidth: 400) - } - .padding() - } - - private func iconName(for variant: GameVariant) -> String { - switch variant { - case .klondike: "suit.spade.fill" - case .spider: "suit.club.fill" - case .freeCell: "suit.diamond.fill" - } - } - - private func subtitle(for variant: GameVariant) -> String { - switch variant { - case .klondike: "Classic solitaire with stock and waste" - case .spider: "Build same-suit sequences with two decks" - case .freeCell: "Strategic play with four free cells" - } - } -} diff --git a/SoliCards/Views/Menu/NewGameSheet.swift b/SoliCards/Views/Menu/NewGameSheet.swift deleted file mode 100644 index d5309fa..0000000 --- a/SoliCards/Views/Menu/NewGameSheet.swift +++ /dev/null @@ -1,79 +0,0 @@ -import SwiftUI - -struct NewGameSheet: View { - @State var variant: GameVariant - @State var difficulty: Difficulty - let onStart: (GameVariant, Difficulty) -> Void - @Environment(\.dismiss) private var dismiss - - var body: some View { - NavigationStack { - Form { - Section("Game") { - Picker("Variant", selection: $variant) { - ForEach(GameVariant.allCases) { v in - Text(v.displayName).tag(v) - } - } - } - - Section("Difficulty") { - Picker("Difficulty", selection: $difficulty) { - ForEach(Difficulty.allCases) { d in - Text(d.displayName).tag(d) - } - } - .pickerStyle(.segmented) - - difficultyDetails - } - - Section { - Button { - onStart(variant, difficulty) - } label: { - Text("Start Game") - .frame(maxWidth: .infinity) - .font(.headline) - } - .buttonStyle(.borderedProminent) - .listRowBackground(Color.clear) - } - } - .navigationTitle("New Game") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } - } - } - } - #if os(macOS) - .frame(width: 400, height: 350) - #endif - } - - private var difficultyDetails: some View { - let settings = difficulty.settings - return VStack(alignment: .leading, spacing: 6) { - if variant == .klondike { - detailRow("Draw", "\(settings.drawCount) card\(settings.drawCount == 1 ? "" : "s")") - } - detailRow("Undos", settings.maxUndos == .max ? "Unlimited" : "\(settings.maxUndos)") - detailRow("Hints", settings.hintsEnabled ? "On" : "Off") - detailRow("Score multiplier", String(format: "%.1fx", settings.scoreMultiplier)) - } - .font(.caption) - .foregroundStyle(.secondary) - } - - private func detailRow(_ label: String, _ value: String) -> some View { - HStack { - Text(label) - Spacer() - Text(value).fontWeight(.medium) - } - } -} diff --git a/SoliCards/Views/Menu/RulesView.swift b/SoliCards/Views/Menu/RulesView.swift deleted file mode 100644 index 47ff9a9..0000000 --- a/SoliCards/Views/Menu/RulesView.swift +++ /dev/null @@ -1,72 +0,0 @@ -import SwiftUI - -struct RulesView: View { - let variant: GameVariant - @Environment(\.dismiss) private var dismiss - - var body: some View { - NavigationStack { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - switch variant { - case .klondike: - klondikeRules - case .spider: - spiderRules - case .freeCell: - freeCellRules - } - } - .padding() - } - .navigationTitle("\(variant.displayName) Rules") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { dismiss() } - } - } - } - } - - private var klondikeRules: some View { - VStack(alignment: .leading, spacing: 12) { - ruleSection("Objective", "Move all 52 cards to the four foundation piles, building each from Ace to King in the same suit.") - ruleSection("Tableau", "Build downward in alternating colors (red on black, black on red). Only Kings can be placed on empty columns.") - ruleSection("Foundation", "Build upward by suit from Ace to King.") - ruleSection("Stock & Waste", "Draw cards from the stock pile. The number drawn depends on difficulty (1 or 3 cards).") - ruleSection("Moving Groups", "You can move a properly sequenced group of face-up cards as a unit.") - } - } - - private var spiderRules: some View { - VStack(alignment: .leading, spacing: 12) { - ruleSection("Objective", "Build eight complete King-to-Ace sequences of the same suit. Completed sequences are automatically removed to the foundation.") - ruleSection("Tableau", "Build downward regardless of suit. However, only same-suit sequences can be moved as a group.") - ruleSection("Stock", "Deal one card to each of the 10 columns. All columns must have at least one card before dealing.") - ruleSection("Completing", "When a complete K through A sequence of the same suit is formed on a tableau, it automatically moves to a foundation pile.") - } - } - - private var freeCellRules: some View { - VStack(alignment: .leading, spacing: 12) { - ruleSection("Objective", "Move all 52 cards to the four foundation piles, building each from Ace to King in the same suit.") - ruleSection("Tableau", "Build downward in alternating colors. Any card can go on an empty column.") - ruleSection("Free Cells", "Four temporary storage cells. Each holds one card at a time.") - ruleSection("Power Moves", "The number of cards you can move at once depends on empty free cells and empty columns: (1 + empty cells) \u{00D7} 2^(empty columns).") - ruleSection("Foundation", "Build upward by suit from Ace to King.") - } - } - - private func ruleSection(_ title: String, _ body: String) -> some View { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.headline) - Text(body) - .font(.body) - .foregroundStyle(.secondary) - } - } -} diff --git a/SoliCards/Views/Settings/CardBackPickerView.swift b/SoliCards/Views/Settings/CardBackPickerView.swift deleted file mode 100644 index 9dce32a..0000000 --- a/SoliCards/Views/Settings/CardBackPickerView.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -struct CardBackPickerView: View { - @Binding var selectedBack: CardBackDesign - - var body: some View { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 60))], spacing: 8) { - ForEach(CardBackDesign.allCases, id: \.self) { design in - Button { - selectedBack = design - } label: { - cardBackImage(design) - .frame(width: 55, height: 77) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .overlay { - if design == selectedBack { - RoundedRectangle(cornerRadius: 4) - .stroke(Color.accentColor, lineWidth: 3) - } - } - } - .buttonStyle(.plain) - } - } - } - - @ViewBuilder - private func cardBackImage(_ design: CardBackDesign) -> some View { - #if os(macOS) - if let image = NSImage(named: design.imageName) { - Image(nsImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - } else { - RoundedRectangle(cornerRadius: 4) - .fill(.blue) - } - #else - if let image = UIImage(named: design.imageName) { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - } else { - RoundedRectangle(cornerRadius: 4) - .fill(.blue) - } - #endif - } -} diff --git a/SoliCards/Views/Settings/CardStylePickerView.swift b/SoliCards/Views/Settings/CardStylePickerView.swift deleted file mode 100644 index 516fef4..0000000 --- a/SoliCards/Views/Settings/CardStylePickerView.swift +++ /dev/null @@ -1,14 +0,0 @@ -import SwiftUI - -struct CardStylePickerView: View { - @Binding var selectedStyle: CardFaceStyle - - var body: some View { - Picker("Card Face Style", selection: $selectedStyle) { - ForEach(CardFaceStyle.allCases, id: \.self) { style in - Text(style.rawValue.capitalized).tag(style) - } - } - .pickerStyle(.segmented) - } -} diff --git a/SoliCards/Views/Settings/SettingsView.swift b/SoliCards/Views/Settings/SettingsView.swift deleted file mode 100644 index a03fded..0000000 --- a/SoliCards/Views/Settings/SettingsView.swift +++ /dev/null @@ -1,45 +0,0 @@ -import SwiftUI - -struct SettingsView: View { - @Binding var theme: GameTheme - @Binding var cardFaceStyle: CardFaceStyle - @Binding var cardBackDesign: CardBackDesign - @Binding var soundEnabled: Bool - @Environment(\.dismiss) private var dismiss - - var body: some View { - NavigationStack { - Form { - Section("Theme") { - ThemePickerView(selectedTheme: $theme) - } - - Section("Card Style") { - Picker("Face Style", selection: $cardFaceStyle) { - ForEach(CardFaceStyle.allCases, id: \.self) { style in - Text(style.rawValue.capitalized).tag(style) - } - } - .pickerStyle(.segmented) - } - - Section("Card Back") { - CardBackPickerView(selectedBack: $cardBackDesign) - } - - Section("Sound") { - Toggle("Sound Effects", isOn: $soundEnabled) - } - } - .navigationTitle("Settings") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { dismiss() } - } - } - } - } -} diff --git a/SoliCards/Views/Settings/ThemePickerView.swift b/SoliCards/Views/Settings/ThemePickerView.swift deleted file mode 100644 index fe1a8f4..0000000 --- a/SoliCards/Views/Settings/ThemePickerView.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -struct ThemePickerView: View { - @Binding var selectedTheme: GameTheme - - var body: some View { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 12) { - ForEach(GameTheme.allThemes) { theme in - Button { - selectedTheme = theme - } label: { - VStack(spacing: 6) { - RoundedRectangle(cornerRadius: 8) - .fill(theme.backgroundColor) - .frame(height: 50) - .overlay { - RoundedRectangle(cornerRadius: 8) - .strokeBorder(theme.accentColor, lineWidth: 2) - } - - Text(theme.displayName) - .font(.caption2) - .lineLimit(1) - } - .overlay { - if theme == selectedTheme { - RoundedRectangle(cornerRadius: 8) - .stroke(Color.accentColor, lineWidth: 3) - .padding(-4) - } - } - } - .buttonStyle(.plain) - } - } - } -} diff --git a/SoliCards/Views/Statistics/StatisticsView.swift b/SoliCards/Views/Statistics/StatisticsView.swift deleted file mode 100644 index 0f31804..0000000 --- a/SoliCards/Views/Statistics/StatisticsView.swift +++ /dev/null @@ -1,63 +0,0 @@ -import SwiftUI -import SwiftData - -struct StatisticsView: View { - @Query private var records: [StatsRecord] - @Environment(\.dismiss) private var dismiss - - var body: some View { - NavigationStack { - List { - if records.isEmpty { - ContentUnavailableView("No Statistics Yet", - systemImage: "chart.bar", - description: Text("Play some games to see your stats here.")) - } else { - ForEach(GameVariant.allCases) { variant in - let variantRecords = records.filter { $0.variant == variant.rawValue } - if !variantRecords.isEmpty { - Section(variant.displayName) { - ForEach(variantRecords, id: \.variant) { record in - statsRow(record) - } - } - } - } - } - } - .navigationTitle("Statistics") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { dismiss() } - } - } - } - } - - private func statsRow(_ record: StatsRecord) -> some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(record.difficulty.capitalized) - .font(.subheadline.bold()) - Spacer() - Text("\(record.gamesWon)/\(record.gamesPlayed) wins") - .font(.caption) - .foregroundStyle(.secondary) - } - HStack { - if record.gamesPlayed > 0 { - Text("Win rate: \(record.winRate * 100, specifier: "%.0f")%") - Spacer() - if record.bestStreak > 0 { - Text("Best streak: \(record.bestStreak)") - } - } - } - .font(.caption) - .foregroundStyle(.secondary) - } - } -} diff --git a/SoliCardsTests/GameEngine/AutoCompleterTests.swift b/SoliCardsTests/GameEngine/AutoCompleterTests.swift deleted file mode 100644 index 8cf75af..0000000 --- a/SoliCardsTests/GameEngine/AutoCompleterTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("AutoCompleter Tests") -struct AutoCompleterTests { - let rules = KlondikeRules() - - @Test("Finds ace to move to empty foundation") - func findAceMove() { - let ace = Card(suit: .hearts, rank: .ace, isFaceUp: true) - var snapshot = emptySnapshot() - snapshot.tableaus[0] = [ace] - - let result = AutoCompleter.findNextAutoMove(state: snapshot, rules: rules) - #expect(result != nil) - #expect(result?.to == .foundation(0)) - } - - @Test("Finds card to build on foundation") - func findFoundationBuild() { - let ace = Card(suit: .hearts, rank: .ace, isFaceUp: true) - let two = Card(suit: .hearts, rank: .two, isFaceUp: true) - var snapshot = emptySnapshot() - snapshot.foundations[0] = [ace] - snapshot.tableaus[0] = [two] - - let result = AutoCompleter.findNextAutoMove(state: snapshot, rules: rules) - #expect(result != nil) - #expect(result?.from == .tableau(0)) - #expect(result?.to == .foundation(0)) - } - - @Test("Returns nil when no moves available") - func noMovesAvailable() { - let snapshot = emptySnapshot() - let result = AutoCompleter.findNextAutoMove(state: snapshot, rules: rules) - #expect(result == nil) - } - - @Test("Finds waste pile ace") - func findWasteAce() { - let ace = Card(suit: .spades, rank: .ace, isFaceUp: true) - var snapshot = emptySnapshot() - snapshot.waste = [ace] - - let result = AutoCompleter.findNextAutoMove(state: snapshot, rules: rules) - #expect(result != nil) - #expect(result?.from == .waste) - } - - @Test("Finds free cell card for FreeCell variant") - func findFreeCellMove() { - let freeCellRules = FreeCellRules() - let ace = Card(suit: .diamonds, rank: .ace, isFaceUp: true) - var snapshot = GameSnapshot( - tableaus: Array(repeating: [], count: 8), - foundations: [[], [], [], []], - stock: [], - waste: [], - freeCells: [ace, nil, nil, nil], - moves: 0, - score: 0 - ) - - let result = AutoCompleter.findNextAutoMove(state: snapshot, rules: freeCellRules) - #expect(result != nil) - #expect(result?.from == .freeCell(0)) - } - - private func emptySnapshot() -> GameSnapshot { - GameSnapshot( - tableaus: Array(repeating: [], count: 7), - foundations: [[], [], [], []], - stock: [], - waste: [], - freeCells: [], - moves: 0, - score: 0 - ) - } -} diff --git a/SoliCardsTests/GameEngine/FreeCellRulesTests.swift b/SoliCardsTests/GameEngine/FreeCellRulesTests.swift deleted file mode 100644 index 35ce2e4..0000000 --- a/SoliCardsTests/GameEngine/FreeCellRulesTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("FreeCell Rules Tests") -struct FreeCellRulesTests { - let rules = FreeCellRules() - - @Test("Deal distributes all 52 cards face-up") - func dealLayout() { - let deck = Deck.standard() - let snapshot = rules.deal(deck: deck) - - #expect(snapshot.tableaus.count == 8) - // First 4 tableaus: 7 cards, last 4: 6 cards - for i in 0..<4 { - #expect(snapshot.tableaus[i].count == 7) - } - for i in 4..<8 { - #expect(snapshot.tableaus[i].count == 6) - } - // All face-up - let allCards = snapshot.tableaus.flatMap { $0 } - #expect(allCards.count == 52) - #expect(allCards.allSatisfy { $0.isFaceUp }) - - // 4 empty free cells - #expect(snapshot.freeCells.count == 4) - #expect(snapshot.freeCells.allSatisfy { $0 == nil }) - - // No stock - #expect(snapshot.stock.isEmpty) - } - - @Test("Power move calculation") - func powerMoves() { - // (1 + empty_freecells) × 2^empty_tableaus - var snapshot = emptySnapshot() - // All 4 free cells empty, target is empty (so 7 empty tableaus count) - let max1 = rules.calculateMaxMovableCards(state: snapshot, targetEmpty: true) - // (1 + 4) × 2^7 = 5 × 128 = 640 - #expect(max1 == 640) - - // 2 free cells occupied, no empty tableaus - snapshot.freeCells = [Card(suit: .hearts, rank: .ace, isFaceUp: true), - Card(suit: .spades, rank: .two, isFaceUp: true), - nil, nil] - for i in 0..<8 { - snapshot.tableaus[i] = [Card(suit: .clubs, rank: .king, isFaceUp: true)] - } - let max2 = rules.calculateMaxMovableCards(state: snapshot, targetEmpty: false) - // (1 + 2) × 2^0 = 3 - #expect(max2 == 3) - } - - @Test("Can move single card to empty free cell") - func moveToFreeCell() { - let card = Card(suit: .hearts, rank: .five, isFaceUp: true) - var snapshot = emptySnapshot() - snapshot.tableaus[0] = [card] - - #expect(rules.canMove(cards: [card], from: .tableau(0), to: .freeCell(0), state: snapshot)) - } - - @Test("Cannot move multiple cards to free cell") - func multipleToFreeCell() { - let card1 = Card(suit: .spades, rank: .six, isFaceUp: true) - let card2 = Card(suit: .hearts, rank: .five, isFaceUp: true) - var snapshot = emptySnapshot() - snapshot.tableaus[0] = [card1, card2] - - #expect(!rules.canMove(cards: [card1, card2], from: .tableau(0), to: .freeCell(0), state: snapshot)) - } - - // MARK: - Helpers - - private func emptySnapshot() -> GameSnapshot { - GameSnapshot( - tableaus: Array(repeating: [], count: 8), - foundations: [[], [], [], []], - stock: [], - waste: [], - freeCells: [nil, nil, nil, nil], - moves: 0, - score: 0 - ) - } -} diff --git a/SoliCardsTests/GameEngine/GameStateTests.swift b/SoliCardsTests/GameEngine/GameStateTests.swift deleted file mode 100644 index 472d45b..0000000 --- a/SoliCardsTests/GameEngine/GameStateTests.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("GameState Tests") -struct GameStateTests { - - @Test("Snapshot captures current state") - func snapshotCapture() { - let state = GameState() - let card = Card(suit: .hearts, rank: .ace, isFaceUp: true) - state.tableaus = [[card]] - state.foundations = [[], [], [], []] - state.moves = 5 - state.score = 100 - - let snapshot = state.snapshot() - #expect(snapshot.tableaus[0].count == 1) - #expect(snapshot.moves == 5) - #expect(snapshot.score == 100) - } - - @Test("Restore from snapshot") - func restoreFromSnapshot() { - let state = GameState() - let card = Card(suit: .spades, rank: .king, isFaceUp: true) - let snapshot = GameSnapshot( - tableaus: [[card]], - foundations: [[], [], [], []], - stock: [], - waste: [], - freeCells: [], - moves: 10, - score: 50 - ) - - state.restore(from: snapshot) - #expect(state.tableaus[0].count == 1) - #expect(state.moves == 10) - #expect(state.score == 50) - } - - @Test("Push and pop history") - func historyStack() { - let state = GameState() - state.tableaus = [[]] - state.foundations = [[], [], [], []] - state.moves = 0 - state.score = 0 - - state.pushHistory() - state.moves = 5 - - #expect(state.canUndo) - let restored = state.popHistory() - #expect(restored != nil) - #expect(restored?.moves == 0) - #expect(!state.canUndo) - } - - @Test("History caps at 20 entries") - func historyCap() { - let state = GameState() - state.tableaus = [[]] - state.foundations = [[], [], [], []] - - for i in 0..<25 { - state.moves = i - state.pushHistory() - } - - #expect(state.history.count == 20) - // First entries should have been removed - #expect(state.history.first?.moves == 5) - } - - @Test("Clear history") - func clearHistory() { - let state = GameState() - state.tableaus = [[]] - state.foundations = [[], [], [], []] - state.pushHistory() - state.pushHistory() - #expect(state.canUndo) - - state.clearHistory() - #expect(!state.canUndo) - #expect(state.history.isEmpty) - } - - @Test("Reset from snapshot sets phase to playing") - func resetSetsPlaying() { - let state = GameState() - let snapshot = GameSnapshot( - tableaus: Array(repeating: [], count: 7), - foundations: [[], [], [], []], - stock: [], - waste: [], - freeCells: [], - moves: 0, - score: 0 - ) - - state.reset(from: snapshot, variant: .klondike) - #expect(state.phase == .playing) - #expect(state.history.isEmpty) - } -} diff --git a/SoliCardsTests/GameEngine/KlondikeRulesTests.swift b/SoliCardsTests/GameEngine/KlondikeRulesTests.swift deleted file mode 100644 index d492955..0000000 --- a/SoliCardsTests/GameEngine/KlondikeRulesTests.swift +++ /dev/null @@ -1,116 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("Klondike Rules Tests") -struct KlondikeRulesTests { - let rules = KlondikeRules() - - @Test("Deal creates correct tableau layout") - func dealLayout() { - let deck = Deck.standard() - let snapshot = rules.deal(deck: deck) - - // 7 tableaus with 1,2,3,4,5,6,7 cards - #expect(snapshot.tableaus.count == 7) - for i in 0..<7 { - #expect(snapshot.tableaus[i].count == i + 1) - // Top card face-up - #expect(snapshot.tableaus[i].last!.isFaceUp) - // All others face-down - for j in 0.. GameSnapshot { - GameSnapshot( - tableaus: Array(repeating: [], count: 7), - foundations: [[], [], [], []], - stock: [], - waste: [], - freeCells: [], - moves: 0, - score: 0 - ) - } -} diff --git a/SoliCardsTests/GameEngine/MoveValidatorTests.swift b/SoliCardsTests/GameEngine/MoveValidatorTests.swift deleted file mode 100644 index 8565676..0000000 --- a/SoliCardsTests/GameEngine/MoveValidatorTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("MoveValidator Tests") -struct MoveValidatorTests { - - @Test("Alternating color detection") - func alternatingColor() { - let redCard = Card(suit: .hearts, rank: .five, isFaceUp: true) - let blackCard = Card(suit: .spades, rank: .four, isFaceUp: true) - let anotherRed = Card(suit: .diamonds, rank: .three, isFaceUp: true) - - #expect(MoveValidator.isAlternatingColor(redCard, with: blackCard)) - #expect(MoveValidator.isAlternatingColor(blackCard, with: redCard)) - #expect(!MoveValidator.isAlternatingColor(redCard, with: anotherRed)) - } - - @Test("Same suit detection") - func sameSuit() { - let spade1 = Card(suit: .spades, rank: .ace, isFaceUp: true) - let spade2 = Card(suit: .spades, rank: .king, isFaceUp: true) - let heart = Card(suit: .hearts, rank: .ace, isFaceUp: true) - - #expect(MoveValidator.isSameSuit(spade1, as: spade2)) - #expect(!MoveValidator.isSameSuit(spade1, as: heart)) - } - - @Test("Ascending rank (for foundations)") - func ascending() { - let ace = Card(suit: .hearts, rank: .ace, isFaceUp: true) - let two = Card(suit: .hearts, rank: .two, isFaceUp: true) - let three = Card(suit: .hearts, rank: .three, isFaceUp: true) - - #expect(MoveValidator.isAscending(two, onto: ace)) - #expect(MoveValidator.isAscending(three, onto: two)) - #expect(!MoveValidator.isAscending(three, onto: ace)) - #expect(!MoveValidator.isAscending(ace, onto: two)) - } - - @Test("Descending rank (for tableaus)") - func descending() { - let king = Card(suit: .spades, rank: .king, isFaceUp: true) - let queen = Card(suit: .hearts, rank: .queen, isFaceUp: true) - let jack = Card(suit: .spades, rank: .jack, isFaceUp: true) - - #expect(MoveValidator.isDescending(queen, onto: king)) - #expect(MoveValidator.isDescending(jack, onto: queen)) - #expect(!MoveValidator.isDescending(king, onto: queen)) - } - - @Test("Can place on foundation") - func canPlaceOnFoundation() { - let ace = Card(suit: .hearts, rank: .ace, isFaceUp: true) - let two = Card(suit: .hearts, rank: .two, isFaceUp: true) - let twoSpades = Card(suit: .spades, rank: .two, isFaceUp: true) - let three = Card(suit: .hearts, rank: .three, isFaceUp: true) - - // Ace on empty foundation - #expect(MoveValidator.canPlaceOnFoundation(ace, topCard: nil)) - // Non-ace on empty foundation - #expect(!MoveValidator.canPlaceOnFoundation(two, topCard: nil)) - // Two of hearts on ace of hearts - #expect(MoveValidator.canPlaceOnFoundation(two, topCard: ace)) - // Two of spades on ace of hearts (wrong suit) - #expect(!MoveValidator.canPlaceOnFoundation(twoSpades, topCard: ace)) - // Three on ace (skipping) - #expect(!MoveValidator.canPlaceOnFoundation(three, topCard: ace)) - } - - @Test("Ace and King detection") - func aceAndKing() { - let ace = Card(suit: .clubs, rank: .ace) - let king = Card(suit: .clubs, rank: .king) - let five = Card(suit: .clubs, rank: .five) - - #expect(MoveValidator.isAce(ace)) - #expect(!MoveValidator.isAce(king)) - #expect(MoveValidator.isKing(king)) - #expect(!MoveValidator.isKing(five)) - } -} diff --git a/SoliCardsTests/GameEngine/SpiderRulesTests.swift b/SoliCardsTests/GameEngine/SpiderRulesTests.swift deleted file mode 100644 index a497389..0000000 --- a/SoliCardsTests/GameEngine/SpiderRulesTests.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("Spider Rules Tests") -struct SpiderRulesTests { - let rules = SpiderRules() - - @Test("Deal creates correct layout with 2 decks") - func dealLayout() { - let deck = Deck.double() - let snapshot = rules.deal(deck: deck) - - #expect(snapshot.tableaus.count == 10) - // First 4 get 6 cards, last 6 get 5 cards - for i in 0..<4 { - #expect(snapshot.tableaus[i].count == 6) - } - for i in 4..<10 { - #expect(snapshot.tableaus[i].count == 5) - } - - // Total dealt: 4*6 + 6*5 = 24 + 30 = 54 - let totalDealt = snapshot.tableaus.reduce(0) { $0 + $1.count } - #expect(totalDealt == 54) - - // Remaining in stock: 104 - 54 = 50 - #expect(snapshot.stock.count == 50) - - // 8 empty foundations - #expect(snapshot.foundations.count == 8) - } - - @Test("Can place any card on descending rank in tableau") - func tableauStacking() { - let ten = Card(suit: .spades, rank: .ten, isFaceUp: true) - let nine = Card(suit: .hearts, rank: .nine, isFaceUp: true) // different suit OK - var snapshot = emptySpiderSnapshot() - snapshot.tableaus[0] = [ten] - snapshot.tableaus[1] = [nine] - - #expect(rules.canMove(cards: [nine], from: .tableau(1), to: .tableau(0), state: snapshot)) - } - - @Test("Complete K-A same-suit sequence detected") - func completeSequence() { - var tableau: [Card] = [] - for rank in Rank.allCases.reversed() { - tableau.append(Card(suit: .spades, rank: rank, isFaceUp: true)) - } - #expect(rules.isCompleteSequence(in: tableau)) - } - - @Test("Mixed-suit sequence not complete") - func mixedSuitSequence() { - var tableau: [Card] = [] - for (i, rank) in Rank.allCases.reversed().enumerated() { - let suit: Suit = i == 5 ? .hearts : .spades - tableau.append(Card(suit: suit, rank: rank, isFaceUp: true)) - } - #expect(!rules.isCompleteSequence(in: tableau)) - } - - @Test("Cannot pick up mixed-suit sequence") - func cannotPickUpMixedSuit() { - let spade5 = Card(suit: .spades, rank: .five, isFaceUp: true) - let heart4 = Card(suit: .hearts, rank: .four, isFaceUp: true) - var snapshot = emptySpiderSnapshot() - snapshot.tableaus[0] = [spade5, heart4] - - #expect(!rules.canPickUp(cards: [spade5, heart4], from: .tableau(0), state: snapshot)) - } - - // MARK: - Helpers - - private func emptySpiderSnapshot() -> GameSnapshot { - GameSnapshot( - tableaus: Array(repeating: [], count: 10), - foundations: Array(repeating: [], count: 8), - stock: [], - waste: [], - freeCells: [], - moves: 0, - score: 0 - ) - } -} diff --git a/SoliCardsTests/Models/CardTests.swift b/SoliCardsTests/Models/CardTests.swift deleted file mode 100644 index 7af5486..0000000 --- a/SoliCardsTests/Models/CardTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("Card Model Tests") -struct CardTests { - - @Test("Card color matches suit") - func cardColor() { - let redCard = Card(suit: .hearts, rank: .ace) - let blackCard = Card(suit: .spades, rank: .king) - - #expect(redCard.color == .red) - #expect(blackCard.color == .black) - } - - @Test("Card accessibility description") - func accessibilityDescription() { - let faceUp = Card(suit: .diamonds, rank: .queen, isFaceUp: true) - let faceDown = Card(suit: .clubs, rank: .two, isFaceUp: false) - - #expect(faceUp.accessibilityDescription == "Queen of Diamonds") - #expect(faceDown.accessibilityDescription == "Card, face down") - } - - @Test("Card identity") - func uniqueIdentity() { - let card1 = Card(suit: .spades, rank: .ace) - let card2 = Card(suit: .spades, rank: .ace) - - // Same suit/rank but different IDs - #expect(card1.id != card2.id) - } - - @Test("Front image name") - func frontImageName() { - let card = Card(suit: .hearts, rank: .king) - #expect(card.frontImageName(style: .classic) == "classic_hearts_king") - #expect(card.frontImageName(style: .modern) == "modern_hearts_king") - } -} diff --git a/SoliCardsTests/Models/DeckTests.swift b/SoliCardsTests/Models/DeckTests.swift deleted file mode 100644 index daedf11..0000000 --- a/SoliCardsTests/Models/DeckTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("Deck Tests") -struct DeckTests { - - @Test("Standard deck has 52 cards") - func standardDeckCount() { - let deck = Deck.standard() - #expect(deck.count == 52) - } - - @Test("Double deck has 104 cards") - func doubleDeckCount() { - let deck = Deck.double() - #expect(deck.count == 104) - } - - @Test("Standard deck has all suits and ranks") - func standardDeckCompleteness() { - let deck = Deck.standard() - for suit in Suit.allCases { - for rank in Rank.allCases { - let count = deck.filter { $0.suit == suit && $0.rank == rank }.count - #expect(count == 1, "Expected exactly 1 \(rank.displayName) of \(suit.displayName)") - } - } - } - - @Test("Deck starts with all cards face down") - func allFaceDown() { - let deck = Deck.standard() - #expect(deck.allSatisfy { !$0.isFaceUp }) - } - - @Test("Shuffle produces different orderings") - func shuffleRandomness() { - let deck1 = Deck.standard() - let deck2 = Deck.standard() - // Extremely unlikely to be identical after shuffle - let sameOrder = zip(deck1, deck2).allSatisfy { $0.suit == $1.suit && $0.rank == $1.rank } - #expect(!sameOrder) - } -} diff --git a/SoliCardsTests/Models/DifficultyTests.swift b/SoliCardsTests/Models/DifficultyTests.swift deleted file mode 100644 index 79b3c97..0000000 --- a/SoliCardsTests/Models/DifficultyTests.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Testing -@testable import SoliCards - -@Suite("Difficulty Tests") -struct DifficultyTests { - - @Test("Easy settings") - func easySettings() { - let settings = Difficulty.easy.settings - #expect(settings.drawCount == 1) - #expect(settings.maxUndos == .max) - #expect(settings.hintsEnabled) - #expect(settings.scoreMultiplier == 0.5) - } - - @Test("Medium settings") - func mediumSettings() { - let settings = Difficulty.medium.settings - #expect(settings.drawCount == 3) - #expect(settings.maxUndos == 20) - #expect(settings.hintsEnabled) - } - - @Test("Hard settings disable hints") - func hardNoHints() { - let settings = Difficulty.hard.settings - #expect(!settings.hintsEnabled) - #expect(settings.maxUndos == 10) - } - - @Test("Expert settings are most restrictive") - func expertSettings() { - let settings = Difficulty.expert.settings - #expect(!settings.hintsEnabled) - #expect(settings.maxUndos == 5) - #expect(settings.scoreMultiplier == 2.0) - } - - @Test("All difficulties have display names") - func displayNames() { - for difficulty in Difficulty.allCases { - #expect(!difficulty.displayName.isEmpty) - } - } -} - -@Suite("GameVariant Tests") -struct GameVariantTests { - - @Test("Klondike properties") - func klondikeProps() { - let v = GameVariant.klondike - #expect(v.tableauCount == 7) - #expect(v.foundationCount == 4) - #expect(v.deckCount == 1) - #expect(v.hasWaste) - #expect(v.hasStock) - #expect(!v.hasFreeCells) - } - - @Test("Spider properties") - func spiderProps() { - let v = GameVariant.spider - #expect(v.tableauCount == 10) - #expect(v.foundationCount == 8) - #expect(v.deckCount == 2) - #expect(!v.hasFreeCells) - } - - @Test("FreeCell properties") - func freeCellProps() { - let v = GameVariant.freeCell - #expect(v.tableauCount == 8) - #expect(v.foundationCount == 4) - #expect(v.deckCount == 1) - #expect(!v.hasWaste) - #expect(!v.hasStock) - #expect(v.hasFreeCells) - #expect(v.freeCellCount == 4) - } -} - -@Suite("Rank Tests") -struct RankTests { - - @Test("Rank ordering") - func rankOrder() { - #expect(Rank.ace < Rank.two) - #expect(Rank.queen < Rank.king) - #expect(Rank.ace.rawValue == 1) - #expect(Rank.king.rawValue == 13) - } - - @Test("All 13 ranks exist") - func allRanks() { - #expect(Rank.allCases.count == 13) - } - - @Test("File names for assets") - func fileNames() { - #expect(Rank.ace.fileName == "ace") - #expect(Rank.two.fileName == "2") - #expect(Rank.ten.fileName == "10") - #expect(Rank.jack.fileName == "jack") - #expect(Rank.queen.fileName == "queen") - #expect(Rank.king.fileName == "king") - } -} - -@Suite("Suit Tests") -struct SuitTests { - - @Test("Red and black suits") - func suitColors() { - #expect(Suit.hearts.color == .red) - #expect(Suit.diamonds.color == .red) - #expect(Suit.spades.color == .black) - #expect(Suit.clubs.color == .black) - } - - @Test("All 4 suits exist") - func allSuits() { - #expect(Suit.allCases.count == 4) - } - - @Test("Suit symbols") - func symbols() { - #expect(Suit.spades.symbol == "♠") - #expect(Suit.hearts.symbol == "♥") - #expect(Suit.diamonds.symbol == "♦") - #expect(Suit.clubs.symbol == "♣") - } -} diff --git a/project.yml b/project.yml deleted file mode 100644 index 9375a34..0000000 --- a/project.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: SoliCards -options: - bundleIdPrefix: com.solicards - deploymentTarget: - iOS: "17.0" - macOS: "14.0" - xcodeVersion: "16.3" - generateEmptyDirectories: true - -settings: - base: - SWIFT_VERSION: "6.0" - ENABLE_USER_SCRIPT_SANDBOXING: true - -targets: - SoliCards: - type: application - supportedDestinations: [iOS, macOS] - sources: - - SoliCards - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: com.solicards.app - GENERATE_INFOPLIST_FILE: true - INFOPLIST_KEY_CFBundleDisplayName: SoliCards - INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.card-games - MARKETING_VERSION: "1.0.0" - CURRENT_PROJECT_VERSION: "1" - SWIFT_EMIT_LOC_STRINGS: "YES" - ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor - INFOPLIST_KEY_UILaunchScreen_Generation: true - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: false - INFOPLIST_KEY_UIRequiresFullScreen: false - INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" - configs: - Debug: - SWIFT_ACTIVE_COMPILATION_CONDITIONS: DEBUG - Release: - SWIFT_COMPILATION_MODE: wholemodule - SWIFT_OPTIMIZATION_LEVEL: "-O" - - SoliCardsTests: - type: bundle.unit-test - supportedDestinations: [iOS, macOS] - sources: - - SoliCardsTests - dependencies: - - target: SoliCards - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: com.solicards.tests - GENERATE_INFOPLIST_FILE: true