# Casino Game Development Guide This guide explains how to build a new casino card game (like Blackjack, Poker, etc.) using CasinoKit, following the patterns established in the Baccarat app. ## Project Structure ``` YourGame/ ├── YourGameApp.swift # App entry point ├── ContentView.swift # Root view (usually just GameTableView) ├── Engine/ │ ├── YourGameEngine.swift # Game rules & logic │ └── GameState.swift # Game state machine ├── Models/ │ ├── BetType.swift # Game-specific bet types │ ├── GameResult.swift # Win/loss/push outcomes │ ├── GameSettings.swift # User settings (can reuse pattern) │ ├── Hand.swift # Hand representation (if needed) │ └── Shoe.swift # Card shoe (if multi-deck) ├── Storage/ │ └── YourGameData.swift # Persistence model (PersistableGameData) ├── Theme/ │ └── DesignConstants.swift # Game-specific design tokens ├── Views/ │ ├── GameTableView.swift # Main game screen │ ├── YourTableLayoutView.swift # Game-specific table layout │ ├── ResultBannerView.swift # Win/loss display │ ├── RulesHelpView.swift # Game rules explanation │ ├── SettingsView.swift # Settings screen │ └── StatisticsSheetView.swift # Stats display └── Resources/ └── Localizable.xcstrings # Translations ``` ## What CasinoKit Provides ### Core Components (Import `CasinoKit`) | Category | Components | Usage | |----------|------------|-------| | **Cards** | `Card`, `Suit`, `Rank`, `Deck` | Card models | | **Card Views** | `CardView`, `CardPlaceholderView` | Card display | | **Chips** | `ChipDenomination`, `ChipView`, `ChipStackView`, `ChipSelectorView` | Betting chips | | **Table** | `TableBackgroundView`, `FeltPatternView` | Casino felt background | | **Overlays** | `GameOverView`, `ConfettiView` | Game over & celebrations | | **Top Bar** | `TopBarView` | Balance, settings, stats buttons | | **Badges** | `ValueBadge` | Numeric value display | | **Settings** | `SettingsToggle`, `SpeedPicker`, `VolumePicker`, `BalancePicker` | Settings UI | | **Sheets** | `SheetContainerView`, `SheetSection` | Modal sheets | | **Branding** | `AppIconView`, `LaunchScreenView` | App icons & splash | | **Audio** | `SoundManager`, `GameSound` | Sound effects & haptics | | **Storage** | `CloudSyncManager`, `PersistableGameData` | iCloud persistence | | **Models** | `TableLimits` | Betting limits presets | | **Design** | `CasinoDesign` | Spacing, colors, animations | ### Using CasinoKit Components ```swift import SwiftUI import CasinoKit struct GameTableView: View { @State private var settings = GameSettings() @State private var gameState: GameState? @State private var selectedChip: ChipDenomination = .hundred var body: some View { ZStack { // 1. Table Background (from CasinoKit) TableBackgroundView() VStack { // 2. Top Bar (from CasinoKit) TopBarView( balance: state.balance, secondaryInfo: "\(state.engine.shoe.cardsRemaining)", secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill", leadingButtons: [ // Add game-specific buttons here (optional) // TopBarButton(icon: "arrow.counterclockwise", accessibilityLabel: "Reset") { // state.resetGame() // } ], onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } ) // 3. Your Game-Specific Table Layout YourTableLayoutView(...) // 4. Chip Selector (from CasinoKit) ChipSelectorView( selectedChip: $selectedChip, availableChips: ChipDenomination.allCases ) // 5. Action Buttons (game-specific) ActionButtonsView(...) } // 6. Result Banner (game-specific, but follows pattern) if state.showResultBanner { ResultBannerView(...) } // 7. Confetti for Wins (from CasinoKit) if state.lastWinnings > 0 { ConfettiView() } // 8. Game Over (from CasinoKit) if state.isGameOver { GameOverView( roundsPlayed: state.roundsPlayed, onPlayAgain: { state.resetGame() } ) } } .sheet(isPresented: $showSettings) { SettingsView(settings: settings, gameState: state) { ... } } } } ``` ## Game-Specific Implementation ### 1. Game Engine (Required) Create your game's rule engine. This handles: - Card dealing logic - Hand evaluation - Win/loss determination - Payout calculations ```swift // Engine/YourGameEngine.swift import CasinoKit @Observable @MainActor final class YourGameEngine { var shoe: Shoe var playerHand: [Card] = [] var dealerHand: [Card] = [] init(deckCount: Int = 6) { self.shoe = Shoe(deckCount: deckCount) } func dealInitialCards() { ... } func evaluateHand(_ cards: [Card]) -> Int { ... } func determineWinner() -> GameResult { ... } func calculatePayout(bet: Int, result: GameResult) -> Int { ... } } ``` ### 2. Game State (Required) Manages the state machine for your game: ```swift // Engine/GameState.swift import SwiftUI import CasinoKit enum GamePhase { case betting case dealing case playerTurn // Blackjack-specific case dealerTurn // Blackjack-specific case roundComplete } @Observable @MainActor final class GameState { // Core state var balance: Int var currentBets: [BetType: Int] = [:] var currentPhase: GamePhase = .betting var showResultBanner = false // Engine let engine: YourGameEngine // Persistence private let persistence: CloudSyncManager // Sound private let sound = SoundManager.shared init(settings: GameSettings) { self.engine = YourGameEngine(deckCount: settings.deckCount.rawValue) self.balance = settings.startingBalance self.persistence = CloudSyncManager() loadSavedGame() } func placeBet(type: BetType, amount: Int) { currentBets[type, default: 0] += amount balance -= amount sound.play(.chipPlace) } func deal() async { currentPhase = .dealing sound.play(.cardDeal) // Game-specific dealing logic } func newRound() { currentPhase = .betting showResultBanner = false sound.play(.newRound) } } ``` ### 3. Bet Types (Game-Specific) ```swift // Models/BetType.swift enum BetType: String, CaseIterable, Identifiable { // Blackjack example: case main = "main" case insurance = "insurance" case doubleDown = "double" case split = "split" // Baccarat example: // case player, banker, tie, playerPair, bankerPair, dragonBonusPlayer, dragonBonusBanker var id: String { rawValue } var displayName: String { ... } var payoutMultiplier: Double { ... } } ``` ### 4. Game Result (Game-Specific) ```swift // Models/GameResult.swift enum GameResult: Equatable { // Blackjack example: case playerWins case dealerWins case push case blackjack case bust var displayText: String { ... } var color: Color { ... } } ``` ### 5. Table Layout (Game-Specific) This is the main visual difference between games: ```swift // Views/YourTableLayoutView.swift struct BlackjackTableView: View { // Shows dealer hand at top, player hand(s) below // Hit/Stand/Double/Split buttons // Insurance betting zone } struct BaccaratTableView: View { // Shows Player/Banker/Tie betting zones // Side bet zones (pairs, dragon bonus) } struct PokerTableView: View { // Community cards in center // Player positions around table // Pot display } ``` ### 6. Settings View (Mostly Reusable) ```swift // Views/SettingsView.swift import CasinoKit struct SettingsView: View { @Bindable var settings: GameSettings let gameState: GameState var body: some View { SheetContainerView(title: "Settings") { // Table Limits (from CasinoKit pattern) SheetSection(title: "TABLE LIMITS", icon: "banknote") { // Use TableLimits enum from CasinoKit } // Deck Settings (game-specific) SheetSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") { // DeckCount options } // Display Settings (reusable) SheetSection(title: "DISPLAY", icon: "eye") { SettingsToggle(title: "...", subtitle: "...", isOn: $settings.showX) } // Sound (from CasinoKit) SheetSection(title: "SOUND & HAPTICS", icon: "speaker.wave.2") { SettingsToggle(...) VolumePicker(volume: $settings.soundVolume) } // iCloud Sync (pattern from Baccarat) SheetSection(title: "CLOUD SYNC", icon: "icloud") { ... } } } } ``` ## Sound Integration ```swift // In your GameState let sound = SoundManager.shared // Play sounds at appropriate moments: sound.play(.chipPlace) // When placing a bet sound.play(.cardDeal) // When dealing cards sound.play(.cardFlip) // When flipping cards sound.play(.win) // On player win sound.play(.lose) // On player loss sound.play(.push) // On tie/push sound.play(.newRound) // Starting new round sound.play(.gameOver) // When out of chips ``` ## Persistence ```swift // Storage/YourGameData.swift import CasinoKit struct BlackjackGameData: PersistableGameData { static let gameIdentifier = "blackjack" var roundsPlayed: Int { roundHistory.count } var lastModified: Date static var empty: BlackjackGameData { BlackjackGameData( lastModified: Date(), balance: 10_000, roundHistory: [], totalWinnings: 0, blackjackCount: 0, // Game-specific stat bustCount: 0 // Game-specific stat ) } var balance: Int var roundHistory: [SavedRoundResult] var totalWinnings: Int var blackjackCount: Int var bustCount: Int } ``` ## Design Constants Extend CasinoDesign for game-specific values: ```swift // Theme/DesignConstants.swift enum Design { // Reuse CasinoDesign values typealias Spacing = CasinoDesign.Spacing typealias CornerRadius = CasinoDesign.CornerRadius typealias Animation = CasinoDesign.Animation // Game-specific sizes enum Size { static let playerCardWidth: CGFloat = 55 static let dealerCardWidth: CGFloat = 50 // ... game-specific dimensions } // Game-specific colors (extend Color) } extension Color { enum BettingZone { static let main = Color.blue.opacity(0.3) static let insurance = Color.yellow.opacity(0.3) // ... game-specific colors } } ``` ## Localization Use String Catalogs (`.xcstrings`): ```swift // Game-specific strings Text(String(localized: "Hit")) Text(String(localized: "Stand")) Text(String(localized: "Double Down")) Text(String(localized: "Split")) Text(String(localized: "Blackjack!")) Text(String(localized: "Bust!")) ``` ## Responsive Layout (iPhone vs iPad) ### The Problem On iPad, content that looks great on iPhone will **stretch to fill the screen**, making: - Betting areas look awkward and disproportionate - Cards appear too spread out - Buttons become oversized - Overlays cover the entire screen unnecessarily ### The Solution: Constrained Width Containers Use `horizontalSizeClass` to detect iPad and constrain content width: ```swift struct GameTableView: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass /// Whether we're on iPad (regular horizontal size class) private var isIPad: Bool { horizontalSizeClass == .regular } /// Maximum content width based on device and orientation private var maxContentWidth: CGFloat { if isIPad { // Landscape on iPad gets more width return verticalSizeClass == .compact ? CasinoDesign.Size.maxContentWidthLandscape // 800pt : CasinoDesign.Size.maxContentWidthPortrait // 500pt } return .infinity // iPhone uses full width } var body: some View { ZStack { TableBackgroundView() VStack { TopBarView(...) .frame(maxWidth: maxContentWidth) // Game table - constrained on iPad YourTableLayoutView(...) .frame(maxWidth: maxContentWidth) // Chip selector - constrained ChipSelectorView(...) .frame(maxWidth: maxContentWidth) // Action buttons - constrained ActionButtonsView(...) .frame(maxWidth: maxContentWidth) } .frame(maxWidth: .infinity) // Centers constrained content // Overlays - full screen background, constrained content if showResultBanner { ResultBannerView(...) } } } } ``` ### Overlay Pattern (Result Banner, Game Over) Overlays need special handling: **full-screen dim background** with **constrained content card**: ```swift struct ResultBannerView: View { var body: some View { ZStack { // 1. Full-screen dark background Color.black.opacity(0.7) .ignoresSafeArea() // 2. Constrained content card VStack { // Your content } .padding() .background(RoundedRectangle(cornerRadius: 24).fill(...)) .frame(maxWidth: CasinoDesign.Size.maxModalWidth) // 450pt // Centered automatically by ZStack } } } ``` ### Fixed-Size Containers (Prevent Layout Shifts) When content changes (cards dealt, buttons appear/disappear), prevent jarring layout shifts: ```swift // ❌ BAD: Container resizes as cards are added HStack { ForEach(cards) { card in CardView(card: card) } } // ✅ GOOD: Fixed container based on max possible content ZStack { // Reserve space for max cards (e.g., 3 cards with overlap) Color.clear .frame(width: calculateMaxWidth(), height: cardHeight) // Actual cards centered within HStack(spacing: cardSpacing) { ForEach(cards) { card in CardView(card: card) } } } ``` Same for buttons: ```swift // ✅ GOOD: Fixed height container for buttons ZStack { Color.clear .frame(height: 60) // Fixed height // Buttons animate in/out within fixed space if showDealButton { ActionButton("Deal", ...) .transition(.scale.combined(with: .opacity)) } } .animation(.spring(duration: 0.3), value: currentPhase) ``` ### Confetti Full-Screen Fix Confetti must cover the entire screen on iPad: ```swift struct ConfettiView: View { var body: some View { GeometryReader { geometry in ZStack { ForEach(0..<50, id: \.self) { _ in ConfettiPiece(containerSize: geometry.size) } } } .ignoresSafeArea() // Critical! .allowsHitTesting(false) } } ``` ### Design Constants for Responsive Layout ```swift // In CasinoDesign.swift enum Size { // Max widths for iPad constraint static let maxContentWidthPortrait: CGFloat = 500 static let maxContentWidthLandscape: CGFloat = 800 static let maxModalWidth: CGFloat = 450 } ``` ### Common Pitfalls | Issue | Symptom | Fix | |-------|---------|-----| | Stretched table | Betting zones look huge on iPad | Add `.frame(maxWidth: maxContentWidth)` | | Overlay too wide | Result banner covers entire iPad screen | Use full-screen bg + constrained content card | | Layout shifts | Cards/buttons cause content to jump | Use fixed-size `ZStack` containers | | Confetti cut off | Only shows in center portion | Use `GeometryReader` + `.ignoresSafeArea()` | | Settings rows cramped | Title/subtitle too close to divider | Add `.padding(.vertical, ...)` | ### Testing Checklist - [ ] iPhone SE (smallest) - [ ] iPhone Pro Max (largest iPhone) - [ ] iPad Portrait - [ ] iPad Landscape - [ ] iPad Split View - [ ] Dynamic Type at maximum accessibility size ## Checklist for New Game ### Setup - [ ] Create new target in Xcode - [ ] Add CasinoKit as dependency - [ ] Copy `DesignConstants.swift` and customize - [ ] Create `Localizable.xcstrings` ### Models - [ ] Define `BetType` enum - [ ] Define `GameResult` enum - [ ] Create `YourGameData` for persistence - [ ] Create `GameSettings` (or reuse pattern) ### Engine - [ ] Implement game rules in `YourGameEngine` - [ ] Implement `GameState` with phases ### Views - [ ] Create `GameTableView` (main container) - [ ] Create game-specific table layout - [ ] Create `ResultBannerView` (follow pattern) - [ ] Create `RulesHelpView` (game rules) - [ ] Customize `SettingsView` - [ ] Create `StatisticsSheetView` ### Integration - [ ] Wire up `SoundManager` for game events - [ ] Implement `CloudSyncManager` for persistence - [ ] Add accessibility labels - [ ] Add localization for all strings ### Polish - [ ] Test Dynamic Type scaling (all sizes including accessibility) - [ ] Test VoiceOver navigation - [ ] Create app icon using `AppIconView` ### Responsive Layout (iPad) - [ ] Add `maxContentWidth` constraint to main views - [ ] Test iPad Portrait - content centered, not stretched - [ ] Test iPad Landscape - wider constraint works well - [ ] Verify overlays: full-screen bg, constrained content - [ ] Fixed-size containers prevent layout shifts - [ ] Confetti covers full screen - [ ] Settings rows have proper padding ## What Could Be Added to CasinoKit The following patterns from Baccarat could be abstracted: 1. **Generic ResultBannerView** - Win/loss display with bet breakdown 2. **BettingZone protocol** - Common betting zone behavior 3. **GameStateProtocol** - Common state machine patterns 4. **HandDisplayView** - Generic card hand display 5. **ActionButtonsView** - Deal/Clear/New Round pattern 6. **StatisticsView** - Generic stats display --- *This guide is based on the Baccarat implementation. For reference, see the Baccarat app structure and CasinoKit source code.*