# CasinoKit A reusable Swift Package for building casino card games with SwiftUI. This package provides common components, themes, and utilities shared across casino game apps. ## Requirements - iOS 17.0+ - Swift 6.0+ - Xcode 16.0+ ## Installation This package is included as a local package in the workspace. To use in another project: 1. Copy the `CasinoKit` folder to your project 2. In Xcode: File → Add Package Dependencies → Add Local 3. Select the `CasinoKit` folder ## Features ### 🎴 Cards **CardView** - A playing card with flip animation support. ```swift import CasinoKit // Face-up card CardView(card: Card(suit: .hearts, rank: .ace), faceUp: true) // Face-down card CardView(card: card, faceUp: false) // Empty placeholder (dotted outline) CardPlaceholderView() ``` **Card Model** ```swift let card = Card(suit: .spades, rank: .king) print(card.displayValue) // "K" print(card.suit.symbol) // "♠" ``` ### 🎰 Chips **ChipView** - A casino chip with denomination display. ```swift ChipView(denomination: .hundred, size: 60, isSelected: true) ``` **ChipSelectorView** - Horizontal chip selector. ```swift @State var selectedChip: ChipDenomination = .hundred ChipSelectorView( denominations: ChipDenomination.allCases, selectedDenomination: $selectedChip ) ``` **ChipStackView** - Stacked chips showing bet amount. ```swift ChipStackView(amount: 500, chipColor: .red) ``` **Chip Denominations** (with standard casino colors) - `.one` ($1) - White/Light Blue - `.five` ($5) - Red - `.twentyFive` ($25) - Green - `.hundred` ($100) - Black - `.fiveHundred` ($500) - Purple - `.thousand` ($1,000) - Yellow - `.fiveThousand` ($5,000) - Brown - `.tenThousand` ($10,000) - Gray (custom) - `.twentyFiveThousand` ($25,000) - Teal (plaque style) - `.hundredThousand` ($100,000) - Burgundy (VIP) ### 📋 Sheets & Popups **SheetContainerView** - Consistent modal sheet styling. ```swift SheetContainerView( title: "Settings", content: { SheetSection(title: "DISPLAY", icon: "eye") { Toggle("Dark Mode", isOn: $darkMode) } SheetSection(title: "SOUND", icon: "speaker.wave.2") { Slider(value: $volume) } }, onCancel: { dismiss() }, onDone: { save(); dismiss() }, doneButtonText: String(localized: "Done"), cancelButtonText: String(localized: "Cancel") ) ``` **SheetSection** - Styled section within sheets. ```swift SheetSection(title: "SECTION TITLE", icon: "star.fill") { // Your content } ``` ### 🎓 Onboarding & Tutorials **WelcomeSheet** - First-launch welcome screen with features list. ```swift WelcomeSheet( gameName: "Blackjack", features: [ WelcomeFeature( icon: "target", title: "Beat the Dealer", description: "Get closer to 21 than the dealer without going over" ), WelcomeFeature( icon: "lightbulb.fill", title: "Learn Strategy", description: "Built-in hints show optimal plays" ) ], onStartTutorial: { // Enable tutorial mode gameState.onboarding.startTutorialMode() }, onStartPlaying: { // Skip to game gameState.onboarding.completeWelcome() } ) ``` **Sherpa Walkthrough** - Guided spotlight walkthrough (re-exported from Sherpa package). ```swift // 1. Define walkthrough steps enum MyTags: SherpaTags { case bettingZone case dealButton func makeCallout() -> Callout { switch self { case .bettingZone: return .localizedLabeled("walkthrough.bettingZone", systemImage: "hand.tap.fill") case .dealButton: return .localizedLabeled("walkthrough.dealButton", systemImage: "play.fill") } } } // 2. Wrap app in SherpaContainerView (in @main App) SherpaContainerView(configuration: .default) { ContentView() } // 3. Tag views and activate walkthrough struct GameTableView: View, SherpaDelegate { @State private var isWalkthroughActive = false var body: some View { VStack { BettingZone() .sherpaTag(MyTags.bettingZone) DealButton() .sherpaTag(MyTags.dealButton) } .sherpa(isActive: isWalkthroughActive, tags: MyTags.self, delegate: self) } func onWalkthroughComplete(sherpa: Sherpa) { isWalkthroughActive = false onboarding.completeWelcome() } } ``` **OnboardingState** - Track onboarding completion status. ```swift let onboarding = OnboardingState(gameIdentifier: "blackjack") // Check if user has seen welcome if !onboarding.hasCompletedWelcome { showWelcome = true } // Mark welcome/walkthrough as completed onboarding.completeWelcome() // Skip onboarding entirely onboarding.skipOnboarding() // Reset onboarding (for testing) onboarding.reset() ``` **PulsingModifier** - Draw attention to interactive elements. ```swift Button("Deal", action: deal) .pulsing( isActive: shouldHighlight, color: .white, scale: 1.3 ) ``` ### 🎲 Game Table Components **TableBackgroundView** - Casino felt background with pattern. ```swift TableBackgroundView() // Default casino green TableBackgroundView(feltColor: .blue, edgeColor: .darkBlue) // Custom ``` **TopBarView** - Balance display and toolbar buttons. ```swift TopBarView( balance: 10_500, secondaryInfo: "411", secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill", onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } ) ``` **TopBarButton** - Add custom buttons to the toolbar. ```swift // Add game-specific buttons at the front of the toolbar TopBarView( balance: 10_500, leadingButtons: [ TopBarButton(icon: "arrow.counterclockwise", accessibilityLabel: "Reset Game") { resetGame() }, TopBarButton(icon: "creditcard", accessibilityLabel: "Buy Chips") { showBuyChips = true } ], onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } ) ``` **HandDisplayView** - Generic card hand display with labels. ```swift HandDisplayView( cards: [card1, card2], cardsFaceUp: [true, true], isWinner: true, label: "PLAYER", value: 21, valueColor: .blue, maxCards: 5 // Reserve space for up to 5 cards ) ``` **BettingZone** - Tappable betting area with chip badge. ```swift BettingZone( label: "PLAYER", payoutInfo: "1:1", betAmount: 500, backgroundColor: .blue.opacity(0.2), onTap: { placeBet(.player) } ) ``` **ActionButton** - Styled action buttons. ```swift ActionButton("Deal", icon: "play.fill", style: .primary) { deal() } ActionButton("Clear", icon: "xmark.circle", style: .destructive) { clear() } ActionButton("Stand", style: .secondary, isEnabled: canStand) { stand() } ``` ### 🎉 Effects & Overlays **ConfettiView** - Win celebration effect. ```swift if playerWon { ConfettiView() // Colorful falling confetti ConfettiView(colors: [.yellow, .gold], count: 30) // Customized } ``` **GameOverView** - Game over modal when out of chips. ```swift GameOverView( roundsPlayed: 25, additionalStats: [ ("Biggest Win", "$5,000"), ("Blackjacks", "3") ], onPlayAgain: { resetGame() } ) ``` **ValueBadge** - Circular numeric badge. ```swift ValueBadge(value: 21, color: .blue) // Hand value display ``` ### ⚙️ Settings Components **SettingsToggle** - Toggle with title and subtitle. ```swift SettingsToggle( title: "Sound Effects", subtitle: "Chips, cards, and results", isOn: $settings.soundEnabled ) ``` **SpeedPicker** - Fast/Normal/Slow animation speed. ```swift SpeedPicker(speed: $settings.dealingSpeed) ``` **VolumePicker** - Volume slider with icons. ```swift VolumePicker(volume: $settings.soundVolume) ``` **BalancePicker** - Starting balance grid. ```swift BalancePicker( balance: $settings.startingBalance, options: [1_000, 5_000, 10_000, 25_000, 50_000, 100_000] ) ``` **TableLimits** - Betting limit presets. ```swift let limits = TableLimits.medium print(limits.minBet) // 25 print(limits.maxBet) // 5000 print(limits.displayName) // "Medium Stakes" ``` ### 🎨 Branding & Icons **AppIconView** - Generate app icons with consistent styling. ```swift // Use a preset AppIconView(config: .baccarat, size: 1024) // Custom configuration let config = AppIconConfig( title: "BLACKJACK", subtitle: "21", iconSymbol: "suit.club.fill", primaryColor: Color(red: 0.1, green: 0.2, blue: 0.35), secondaryColor: Color(red: 0.05, green: 0.12, blue: 0.25), accentColor: .yellow ) AppIconView(config: config, size: 1024) ``` **Preset Configurations:** - `.baccarat` - Spade symbol - `.blackjack` - Club symbol with "21" subtitle - `.poker` - Diamond symbol, red accent - `.roulette` - Grid symbol, red theme **LaunchScreenView** - Animated splash screen. ```swift LaunchScreenView(config: .baccarat) ``` **IconRenderer** - Render views to images. ```swift // Render single icon let image = IconRenderer.renderAppIcon(config: .baccarat, size: 1024) // Render all iOS sizes let allImages = IconRenderer.renderAllSizes(config: .baccarat) ``` ### 🔊 Audio & Haptics **SoundManager** - Manages game sounds and haptic feedback. ```swift // Access shared instance let sound = SoundManager.shared // Configure settings sound.soundEnabled = true sound.hapticsEnabled = true sound.volume = 1.0 // Play sounds sound.play(.chipPlace) sound.play(.cardDeal) sound.play(.win) // Convenience methods (include haptics) sound.playChipPlace() // Chip sound + light haptic sound.playCardFlip() // Card sound + light haptic sound.playWin() // Win sound + success haptic sound.playLose() // Lose sound + error haptic sound.playGameOver() // Game over sound + error haptic ``` **Available Sounds:** | Sound | Description | |-------|-------------| | `.chipPlace` | Placing a bet | | `.chipStack` | Stacking chips | | `.cardDeal` | Dealing a card | | `.cardFlip` | Flipping a card | | `.cardShuffle` | Shuffling deck | | `.win` | Player wins | | `.lose` | Player loses | | `.push` | Tie/push | | `.bigWin` | Large payout | | `.buttonTap` | UI tap | | `.newRound` | Starting new round | | `.clearBets` | Clearing bets | | `.gameOver` | Out of chips | **Custom Sound Files:** The manager uses iOS system sounds as fallback. To use custom sounds: 1. Add `.mp3` files named: `chip_place.mp3`, `card_deal.mp3`, `win.mp3`, etc. 2. Add them to your app bundle's Resources folder 3. Files are automatically detected and used **Haptic Methods:** ```swift sound.hapticLight() // Light tap sound.hapticMedium() // Medium tap sound.hapticHeavy() // Heavy tap sound.hapticSuccess() // Success notification sound.hapticError() // Error notification sound.hapticWarning() // Warning notification ``` ### 📊 Session Management **GameSession** - Track play sessions with common and game-specific stats. ```swift // Create a game-specific stats type struct MyGameStats: GameSpecificStats { var specialWins: Int = 0 init() {} var displayItems: [StatDisplayItem] { [StatDisplayItem(icon: "star.fill", iconColor: .yellow, label: "Special Wins", value: "\(specialWins)")] } } // Create session type alias typealias MyGameSession = GameSession ``` **SessionManagedGame Protocol** - Add session management to your game state. ```swift @Observable class GameState: SessionManagedGame { typealias Stats = MyGameStats var currentSession: MyGameSession? var sessionHistory: [MyGameSession] = [] // Record round results func completeRound(winnings: Int, bet: Int) { recordSessionRound( winnings: winnings, betAmount: bet, outcome: winnings > 0 ? .win : .lose ) { stats in if wasSpecialWin { stats.specialWins += 1 } } } } ``` **Session UI Components:** ```swift // End session button EndSessionButton { state.showEndSessionConfirmation = true } // Current session header with live stats CurrentSessionHeader( duration: session.duration, roundsPlayed: session.roundsPlayed, netResult: session.netResult, onEndSession: { /* confirm */ } ) // Session row for history list SessionSummaryRow( styleDisplayName: "Vegas Strip", duration: session.duration, roundsPlayed: session.roundsPlayed, netResult: session.netResult, startTime: session.startTime, isActive: false, endReason: .manualEnd ) ``` **SessionFormatter** - Format session data for display. ```swift SessionFormatter.formatDuration(3600) // "01h 00min" SessionFormatter.formatMoney(500) // "$500" SessionFormatter.formatMoney(-250) // "-$250" SessionFormatter.formatPercent(65.5) // "65.5%" ``` For detailed documentation, see [SESSION_SYSTEM.md](SESSION_SYSTEM.md). ### 💾 Cloud Storage **CloudSyncManager** - Saves game data locally and syncs with iCloud. ```swift // 1. Define your game's data structure struct MyGameData: PersistableGameData { static let gameIdentifier = "mygame" var roundsPlayed: Int { rounds.count } var lastModified: Date static var empty: MyGameData { MyGameData(rounds: [], balance: 10000, lastModified: Date()) } var rounds: [RoundData] var balance: Int } // 2. Create sync manager let persistence = CloudSyncManager() // 3. Save data (auto-syncs to iCloud) var data = persistence.data data.balance = 5000 persistence.save(data) // 4. Data is automatically loaded on init print(persistence.data.balance) // 5. Check sync status if persistence.iCloudAvailable { print("Last sync: \(persistence.lastSyncDate)") } // 6. Force sync persistence.sync() // 7. Listen for changes from other devices persistence.onCloudDataReceived = { newData in print("Got \(newData.roundsPlayed) rounds from iCloud") } // 8. Reset all data persistence.reset() ``` **PersistableGameData Protocol:** ```swift public protocol PersistableGameData: Codable, Sendable { static var gameIdentifier: String { get } // e.g., "baccarat" var roundsPlayed: Int { get } // For conflict resolution var lastModified: Date { get set } // Updated automatically static var empty: Self { get } // Default/new game state } ``` **Features:** - 📱 **Local Storage** - Always saved to UserDefaults - ☁️ **iCloud Sync** - Automatic sync when signed in - 🔄 **Conflict Resolution** - Uses `roundsPlayed` to pick newer data - 📢 **Change Notifications** - Callbacks when data changes from other devices - 🔒 **Privacy** - Uses Apple ID, no Game Center required ### 📈 Analytics **AnalyticsService** - Lightweight event tracking with a configurable endpoint. ```swift let analytics: AnalyticsTracking = AnalyticsService() await analytics.track( AnalyticsEvent( name: "round_started", properties: [ "game": .string("blackjack"), "tableLimit": .int(25) ] ) ) ``` **Configure an endpoint later:** ```swift await analytics.updateEndpoint(URL(string: "https://example.com/analytics")) ``` **No-op tracker for disabled analytics:** ```swift let analytics: AnalyticsTracking = NoOpAnalyticsTracker() ``` ### 🎨 Design System **CasinoDesign** - Shared design constants. ```swift // Spacing CasinoDesign.Spacing.small // 8 CasinoDesign.Spacing.medium // 12 CasinoDesign.Spacing.large // 16 // Corner Radius CasinoDesign.CornerRadius.small // 8 CasinoDesign.CornerRadius.medium // 12 CasinoDesign.CornerRadius.large // 16 // Font Sizes (base values for @ScaledMetric) CasinoDesign.BaseFontSize.small // 12 CasinoDesign.BaseFontSize.body // 14 CasinoDesign.BaseFontSize.large // 20 // Opacity CasinoDesign.Opacity.subtle // 0.05 CasinoDesign.Opacity.light // 0.2 CasinoDesign.Opacity.medium // 0.5 CasinoDesign.Opacity.heavy // 0.8 // Animation CasinoDesign.Animation.quick // 0.2 CasinoDesign.Animation.standard // 0.3 CasinoDesign.Animation.springDuration // 0.4 ``` **Color.Sheet** - Sheet/popup colors. ```swift Color.Sheet.background // Dark background Color.Sheet.sectionFill // Section card fill Color.Sheet.accent // Yellow accent Color.Sheet.secondaryText // Muted text Color.Sheet.cancelText // Cancel button text ``` ### 🌍 Localization CasinoKit includes localization for: - English (en) - Spanish - Mexico (es-MX) - French - Canada (fr-CA) **Localized Strings:** - Card names (Ace, King, Queen, etc.) - Suit names (Hearts, Diamonds, Clubs, Spades) - Chip-related strings - Accessibility labels **Adding Localizations:** The package uses String Catalogs (`.xcstrings`). Edit: ``` CasinoKit/Sources/CasinoKit/Resources/Localizable.xcstrings ``` ## Usage in a New Game ### 1. Import the Package ```swift import SwiftUI import CasinoKit ``` ### 2. Create Your Game View ```swift struct BlackjackTableView: View { @State private var deck = Deck(numberOfDecks: 6) @State private var selectedChip: ChipDenomination = .hundred var body: some View { VStack { // Player's hand HStack { ForEach(playerCards) { card in CardView(card: card, faceUp: true) } } // Chip selector ChipSelectorView( denominations: ChipDenomination.allCases, selectedDenomination: $selectedChip ) } } } ``` ### 3. Create Settings Sheet ```swift struct SettingsView: View { @Environment(\.dismiss) var dismiss var body: some View { SheetContainerView(title: "Settings") { SheetSection(title: "GAME OPTIONS", icon: "gearshape") { // Your settings } } onDone: { dismiss() } } } ``` ### 4. Generate App Icon ```swift // In a preview or development view #Preview { let config = AppIconConfig( title: "BLACKJACK", subtitle: "21", iconSymbol: "suit.club.fill" ) return AppIconView(config: config, size: 512) } ``` Screenshot the preview and add to your Assets.xcassets. ## File Structure ``` CasinoKit/ ├── Package.swift ├── README.md ├── GAME_TEMPLATE.md # Guide for creating new games ├── SESSION_SYSTEM.md # Session tracking documentation ├── Sources/CasinoKit/ │ ├── CasinoKit.swift │ ├── Exports.swift │ ├── Models/ │ │ ├── Card.swift │ │ ├── Deck.swift │ │ ├── ChipDenomination.swift │ │ ├── TableLimits.swift # Betting limit presets │ │ ├── OnboardingState.swift # Onboarding tracking │ │ └── Session/ │ │ ├── GameSession.swift # Generic session with stats │ │ ├── GameSessionProtocol.swift # Session protocols │ │ └── SessionFormatter.swift # Formatting utilities │ ├── Views/ │ │ ├── Cards/ │ │ │ ├── CardView.swift │ │ │ └── HandDisplayView.swift # Generic hand display │ │ ├── Chips/ │ │ │ ├── ChipView.swift │ │ │ ├── ChipSelectorView.swift │ │ │ ├── ChipStackView.swift │ │ │ └── ChipOnTableView.swift │ │ ├── Sheets/ │ │ │ └── SheetContainerView.swift │ │ ├── Onboarding/ │ │ │ ├── WelcomeSheet.swift # First-launch welcome │ │ │ └── PulsingModifier.swift # Attention-grabbing pulse │ │ ├── Branding/ │ │ │ ├── AppIconView.swift │ │ │ ├── LaunchScreenView.swift │ │ │ └── IconRenderer.swift │ │ ├── Effects/ │ │ │ └── ConfettiView.swift # Win celebration │ │ ├── Overlays/ │ │ │ └── GameOverView.swift # Game over modal │ │ ├── Table/ │ │ │ └── TableBackgroundView.swift # Felt background │ │ ├── Bars/ │ │ │ └── TopBarView.swift # Balance & toolbar │ │ ├── Badges/ │ │ │ └── ValueBadge.swift # Numeric badge │ │ ├── Buttons/ │ │ │ └── ActionButton.swift # Deal/Hit/Stand buttons │ │ ├── Zones/ │ │ │ └── BettingZone.swift # Tappable betting area │ │ ├── Settings/ │ │ │ └── SettingsComponents.swift # Toggle, pickers │ │ └── Session/ │ │ └── SessionViews.swift # Session UI components │ ├── Audio/ │ │ └── SoundManager.swift │ ├── Analytics/ │ │ ├── Models/ │ │ │ ├── AnalyticsEvent.swift │ │ │ └── AnalyticsValue.swift │ │ ├── Protocols/ │ │ │ └── AnalyticsTracking.swift │ │ └── Services/ │ │ ├── AnalyticsService.swift │ │ └── NoOpAnalyticsTracker.swift │ ├── Storage/ │ │ └── CloudSyncManager.swift # iCloud persistence │ ├── Theme/ │ │ ├── CasinoTheme.swift │ │ └── CasinoDesign.swift │ └── Resources/ │ ├── Localizable.xcstrings │ └── Sounds/ │ └── (audio files) └── Tests/CasinoKitTests/ └── CasinoKitTests.swift ``` ## Best Practices ### Design Constants Always use `CasinoDesign` constants instead of magic numbers: ```swift // ✅ Good .padding(CasinoDesign.Spacing.medium) .opacity(CasinoDesign.Opacity.heavy) // ❌ Bad .padding(12) .opacity(0.8) ``` ### Localization Always pass localized strings for button text: ```swift SheetContainerView( title: String(localized: "Settings"), content: { ... }, onDone: { dismiss() }, doneButtonText: String(localized: "Done") ) ``` ### Accessibility All components include VoiceOver support: - Cards announce suit and rank - Chips announce denomination - Interactive elements have labels and hints ### Dynamic Type Use `@ScaledMetric` with base font sizes: ```swift @ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = CasinoDesign.BaseFontSize.body ``` ## Apps Using CasinoKit - **Blackjack** - Classic 21 with basic strategy hints and card counting - **Baccarat** - The classic casino card game with road maps ## Version History - **1.1.0** - Session system - Generic `GameSession` for tracking play sessions - `SessionManagedGame` protocol for easy integration - Session UI components (header, summary rows, end button) - `SessionFormatter` for consistent data display - Aggregated statistics across sessions - **1.0.0** - Initial release - Card and Chip components - Sheet container views - App icon and launch screen generators - Localization support (EN, ES-MX, FR-CA) ## License This package is for personal use in your casino game projects.