926 lines
23 KiB
Markdown
926 lines
23 KiB
Markdown
# 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.
|