first commit

This commit is contained in:
Matt Bruce 2026-02-18 14:52:58 -06:00
commit c2fa745598

925
README.md Normal file
View File

@ -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<MyGameStats>
```
**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<MyGameData>()
// 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<Stats>` 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.