- Move .git from Baccarat/ to CasinoGames root - Add Blackjack game project - Add CasinoKit shared framework - Add .gitignore for Xcode/Swift projects - Add Agents.md and GAME_TEMPLATE.md documentation - Add CasinoGames.xcworkspace
662 lines
19 KiB
Markdown
662 lines
19 KiB
Markdown
# 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",
|
|
onReset: { 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<YourGameData>
|
|
|
|
// Sound
|
|
private let sound = SoundManager.shared
|
|
|
|
init(settings: GameSettings) {
|
|
self.engine = YourGameEngine(deckCount: settings.deckCount.rawValue)
|
|
self.balance = settings.startingBalance
|
|
self.persistence = CloudSyncManager<YourGameData>()
|
|
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.*
|
|
|