- 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
19 KiB
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
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
// 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:
// 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)
// 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)
// 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:
// 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)
// 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
// 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
// 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:
// 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):
// 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:
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:
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:
// ❌ 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:
// ✅ 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:
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
// 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.swiftand customize - Create
Localizable.xcstrings
Models
- Define
BetTypeenum - Define
GameResultenum - Create
YourGameDatafor persistence - Create
GameSettings(or reuse pattern)
Engine
- Implement game rules in
YourGameEngine - Implement
GameStatewith 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
SoundManagerfor game events - Implement
CloudSyncManagerfor 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
maxContentWidthconstraint 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:
- Generic ResultBannerView - Win/loss display with bet breakdown
- BettingZone protocol - Common betting zone behavior
- GameStateProtocol - Common state machine patterns
- HandDisplayView - Generic card hand display
- ActionButtonsView - Deal/Clear/New Round pattern
- StatisticsView - Generic stats display
This guide is based on the Baccarat implementation. For reference, see the Baccarat app structure and CasinoKit source code.