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/CHANGELOG.md b/CHANGELOG.md index 74feada..1c0ab0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,77 +2,11 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [1.2.0] - 2026-04-14 - -### Fixed -- **macOS maximized window cutoff** — cards no longer overflow the bottom when the window is maximized. Height calculation uses 92% of available space as buffer for platform chrome and spacing. -- **macOS card size cap** — maximum card width capped at 120pt on macOS to prevent oversized cards on large displays. Extra space distributed as wider inter-card gaps. -- **iOS landscape boost scoped to iOS only** — the 30% landscape card size boost and 130% scroll content height no longer apply on macOS, where they caused overflow. - -## [1.1.0] - 2026-04-14 - -### Changed -- **Drag and drop rewritten** — replaced `DragGesture` offset-based system with absolute position tracking (`dragPosition: CGPoint`). Cards now follow the finger exactly instead of appearing displaced. -- **Drop target resolution moved to ViewModel** — `endDrag(at:)` method checks drop targets directly, eliminating the need for a separate global gesture on the board. -- **Long press + drag** — card dragging now requires a 0.15s long press before drag begins, disambiguating from scroll gestures and tap-to-move. -- **Gesture priority fixed** — card drag uses `.simultaneousGesture()` instead of `.gesture()` so tap-to-move (`onTapGesture`) is never blocked. Tapping an ace now reliably moves it to the foundation. -- **Bottom toolbar redesigned (iOS)** — reduced from 9 cramped icons to 4 clear items with labels: New, Undo, Hint, More (menu). Secondary actions (variant, difficulty, auto-complete, sound, rules, stats, settings) consolidated into the "More" menu. -- **Toolbar contrast fixed** — bottom bar uses `.ultraThinMaterial` background with `.toolbarColorScheme(.dark)` so icons are readable against all game themes. -- **Top row aligned to tableau columns** — stock at column 0, waste at column 1, gap at column 2, foundations at columns 3–6. No more `Spacer()` pushing elements to opposite edges. - -### Added -- **Smart zoom** — card size dynamically adapts to the actual deepest tableau column so the entire board fits on screen without scrolling. Cards shrink as columns grow during play, and expand when columns shorten. -- **Landscape mode improvements:** - - 30% larger cards than the base height calculation - - Extra horizontal space distributed as wider inter-card gaps to fill the screen - - Vertical scrolling with long-press drag disambiguation (swipe = scroll, hold + drag = move card) - - Scroll content sized to 130% of viewport to guarantee scrollability - - Canvas background responds to scroll gestures (`.contentShape(Rectangle())`) - - Scroll automatically disabled during card drags to prevent interference -- **Stock pile recycling indicator** — empty stock shows an `arrow.clockwise` icon so users know to tap to recycle the waste pile. - -### Fixed -- **Cards no longer displaced during drag** — root cause was overlay positioned with `.offset(translation)` from ZStack origin instead of `.position(location)` at the finger. -- **Tap-to-move works on iPad** — `.simultaneousGesture()` prevents long press from swallowing taps. -- **ScrollView no longer fights card drags** — `.scrollDisabled()` activates when a card drag is in progress. -- **Score bar no longer steals card area** — moved outside `GeometryReader` so card layout measures the actual play area, not the area minus an inaccurate chrome guess. -- **Static analyzer warnings resolved** — removed unused `freeCellsEmpty` variable, unused `destTableau` binding, fixed `@MainActor` isolation on `HapticManager`. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [1.0.0] - 2026-04-13 ### Added -- **Three solitaire variants:** Klondike, Spider (2-deck), and FreeCell with full rule implementations -- **Four difficulty levels:** Easy, Medium, Hard, Expert — each with distinct draw counts, undo limits, hint availability, and score multipliers -- **Drag and drop** card movement using SwiftUI DragGesture with coordinate-space hit-testing -- **Tap to move** cards to the best available destination (foundation preferred) -- **Undo system** with up to 20 states of history -- **Hint system** with 5-priority move suggestions (aces first, then foundations, then tableau reveals) -- **Auto-complete** detection and animated execution when all remaining cards are face-up -- **Win detection** with animated victory overlay -- **Six color themes:** Classic Green, Dark Mode, Ocean Blue, Royal Purple, Forest Green, Sunset Orange -- **12 card back designs** and 3 card face styles (classic, modern, extracted) -- **Game persistence:** auto-save on every move (debounced 2s), resume on app relaunch -- **Statistics tracking:** wins, losses, best score, best time, current/best streak per variant per difficulty -- **User preferences:** theme, card style, card back, sound toggle — all persisted via SwiftData -- **Sound effects:** card flip, place, error, victory (AVFoundation) -- **Haptic feedback** on iOS (UIImpactFeedbackGenerator) -- **Full VoiceOver accessibility:** labels, hints, and traits on all interactive elements -- **Dynamic Type** support on all text -- **Reduce Motion** support — animations skipped when user preference is enabled -- **Localization infrastructure:** String Catalog (Localizable.xcstrings) with 40+ strings, English source -- **Keyboard shortcuts:** Cmd+Z (undo), Cmd+N (new game), H (hint) -- **Privacy manifest** (PrivacyInfo.xcprivacy): no tracking, no data collection -- **App icon** with all required sizes for iOS and macOS -- **57 unit tests** across 12 test suites covering models, game engine, move validation, auto-complete, and game state -- **Multiplatform support:** iOS 17+, iPadOS 17+, macOS 14+ from a single target - -### Architecture -- MVVM with Protocol-Oriented Strategy pattern -- `GameRules` protocol with `KlondikeRules`, `SpiderRules`, `FreeCellRules` implementations -- `@Observable` macro (Observation framework) for property-level UI updates -- `@MainActor` isolation for thread safety -- SwiftData for persistence (GameRecord, StatsRecord, PrefsRecord) -- Zero external dependencies +- Initial workflow template with 8-phase methodology (PROMPT.md) +- CLAUDE.md guidance for Claude Code +- Successfully used to build [SoliCards-iOS-iPadOS-MacOS](https://git.istratai.cloud/aj/SoliCards-iOS-iPadOS-MacOS) diff --git a/CLAUDE.md b/CLAUDE.md index 6b541e9..93d5cd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,51 +2,40 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## What This Project Is +## What This Repository Is -SoliCards is a native SwiftUI solitaire card game for iOS 17+, iPadOS 17+, and macOS 14+. It was ported from a web-based JavaScript game (`game-SoliCards/SoliCards.html`) following the 7-phase workflow defined in [PROMPT.md](PROMPT.md). +This is a **workflow template** for building App Store-ready iOS/macOS applications using Xcode and Claude Code. The core artifact is [PROMPT.md](PROMPT.md) — a structured, 8-phase methodology prompt that guides Claude Code through transforming existing code (prototype, partial app, web logic) into a production-quality Apple platform application. -## Build Commands +Use this repo by copying or referencing `PROMPT.md` as a system/task prompt when starting a new Xcode project with Claude Code. -```bash -# Generate Xcode project (required after cloning or editing project.yml) -xcodegen generate +## Workflow Phases (defined in PROMPT.md) -# Build for iOS Simulator -xcodebuild build -project SoliCards.xcodeproj -scheme SoliCards -destination 'platform=iOS Simulator,name=iPhone 16' +| Phase | Name | Key Gate | +|-------|------|----------| +| 0 | Codebase Intake & Audit | Full read of target folder; audit report approved before any code | +| 1 | Architecture & Project Setup | Architecture pattern, Xcode scaffold, SwiftLint; confirmed before Phase 2 | +| 2 | Core Feature Implementation | One feature at a time; build must pass after each feature | +| 3 | Data, Persistence & Networking | async/await only; secrets in Keychain | +| 4 | Accessibility & Localization | Accessibility Inspector sign-off required | +| 5 | Performance & Quality | Instruments profiling; >=70% test coverage; all tests pass | +| 6 | App Store Preparation | Archive + TestFlight validation | +| 7 | Handoff Documentation | SETUP.md, CHANGELOG.md, ARCHITECTURE.md | -# Build for macOS -xcodebuild build -project SoliCards.xcodeproj -scheme SoliCards -destination 'platform=macOS' CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO +## Core Constraints -# Run tests -xcodebuild test -project SoliCards.xcodeproj -scheme SoliCardsTests -destination 'platform=iOS Simulator,name=iPhone 16' +- **Never skip or combine phases** without explicit user approval. +- **Never mark a phase complete** if there are compiler errors, warnings, or failing tests. +- **Always show a summary** of what you're about to do and wait for approval before acting. +- **Stop and ask** when encountering ambiguity — never assume and proceed. -# Static analysis -xcodebuild analyze -project SoliCards.xcodeproj -scheme SoliCards -destination 'platform=iOS Simulator,name=iPhone 16' -``` +## Technology Defaults -## Architecture +- **Language**: Swift, **UI**: SwiftUI, **Concurrency**: async/await +- **Persistence**: SwiftData, **Secrets**: Keychain only +- **Colors**: Semantic assets only, **Typography**: Dynamic Type +- **Touch targets**: 44x44pt minimum +- **Build target**: Latest stable Xcode + iOS/macOS SDK -**MVVM + Protocol-Oriented Strategy** — see [ARCHITECTURE.md](ARCHITECTURE.md) for full details. +## Example Project -- `GameRules` protocol with 3 conforming structs: `KlondikeRules`, `SpiderRules`, `FreeCellRules` -- `@Observable` macro (not `ObservableObject`) for property-level SwiftUI observation -- `@MainActor` on ViewModels and TimerService for Swift 6 concurrency safety -- `DragGesture` + `PreferenceKey` frame hit-testing for card drag & drop (not `onDrag`/`onDrop`) -- SwiftData for persistence (`GameRecord`, `StatsRecord`, `PrefsRecord`) -- Zero external dependencies - -## Key Conventions - -- `.xcodeproj` is generated by xcodegen from `project.yml` — do not edit it by hand -- `Array.trailingSuffix(while:)` extension replaces missing stdlib `suffix(while:)` — used in rules engines for face-up card runs -- Card images are loaded from the asset catalog via platform-conditional helpers (`UIImage`/`NSImage`) in `CardView` -- Sound playback is dispatched via `nonisolated func playSound()` to avoid `@MainActor` isolation on the audio path - -## Test Suite - -57 tests across 12 suites. All tests are in `SoliCardsTests/` using Swift Testing (`@Test`, `#expect`). - -Key test coverage areas: -- `GameEngine/` — KlondikeRules, SpiderRules, FreeCellRules, MoveValidator, AutoCompleter, GameState -- `Models/` — Card, Deck, Difficulty, GameVariant, Rank, Suit +This workflow was used to build [SoliCards-iOS-iPadOS-MacOS](https://git.istratai.cloud/aj/SoliCards-iOS-iPadOS-MacOS) — a native SwiftUI solitaire game ported from a web-based JavaScript app. diff --git a/README.md b/README.md index e69de29..291026f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,31 @@ +# XCode-Claude-Workflow + +A structured 8-phase workflow for building App Store-ready iOS/macOS applications using **Xcode + Claude Code**. + +## What This Is + +A methodology prompt ([PROMPT.md](PROMPT.md)) that guides Claude Code through transforming existing code — prototypes, web apps, partial implementations — into production-quality native Apple applications. + +## How to Use + +1. Clone this repo +2. Point Claude Code to your existing code folder +3. Paste or reference `PROMPT.md` as the task prompt +4. Follow the 8 phases: Audit → Architecture → Implementation → Persistence → Accessibility → Quality → App Store → Documentation + +## Phases + +| # | Phase | What Happens | +|---|-------|-------------| +| 0 | Codebase Intake | Read every file, produce audit report, confirm platform | +| 1 | Architecture | Propose pattern (MVVM/TCA), scaffold Xcode project | +| 2 | Core Features | Implement one feature at a time with tests | +| 3 | Data & Persistence | SwiftData, networking, error states | +| 4 | Accessibility | VoiceOver, Dynamic Type, localization | +| 5 | Performance | Instruments profiling, 70%+ test coverage | +| 6 | App Store Prep | Privacy manifest, icons, archive, metadata | +| 7 | Documentation | SETUP.md, CHANGELOG.md, ARCHITECTURE.md | + +## Example + +This workflow was used to build [SoliCards-iOS-iPadOS-MacOS](https://git.istratai.cloud/aj/SoliCards-iOS-iPadOS-MacOS) — a solitaire card game ported from JavaScript to native SwiftUI for iOS, iPadOS, and macOS. 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