commit c2fa745598c4982bdbaf2ae3ac4816fe2a147b57 Author: Matt Bruce Date: Wed Feb 18 14:52:58 2026 -0600 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..22d86ef --- /dev/null +++ b/README.md @@ -0,0 +1,925 @@ +# 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.