CasinoKit/SESSION_SYSTEM.md
Matt Bruce e8f54f9d54 moved from casino games
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2026-02-18 14:53:21 -06:00

13 KiB

CasinoKit Session System

This document describes the generic session tracking system in CasinoKit that can be used by any casino game.

Overview

The session system provides:

  • Session tracking: Track play sessions from start to end
  • Common statistics: Wins, losses, pushes, bets, duration
  • Game-specific statistics: Each game can track its own custom stats
  • Session history: Store and display completed sessions
  • Aggregated statistics: Combine stats across all sessions

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         CasinoKit                                │
├─────────────────────────────────────────────────────────────────┤
│  GameSession<Stats>          - Generic session with custom stats │
│  GameSpecificStats           - Protocol for game stats          │
│  SessionManagedGame          - Protocol for game state classes  │
│  AggregatedSessionStats      - Combined stats across sessions   │
│  SessionFormatter            - Formatting utilities             │
│  UI Components               - Reusable session UI views        │
└─────────────────────────────────────────────────────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    │                               │
            ┌───────▼───────┐               ┌───────▼───────┐
            │   Blackjack   │               │   Baccarat    │
            ├───────────────┤               ├───────────────┤
            │ BlackjackStats│               │ BaccaratStats │
            │ (implements   │               │ (implements   │
            │ GameSpecific) │               │ GameSpecific) │
            └───────────────┘               └───────────────┘

Core Types

GameSession

A generic session that works with any game-specific stats type.

public struct GameSession<Stats: GameSpecificStats>: Codable, Identifiable {
    // Identity
    let id: UUID
    let gameStyle: String
    
    // Timing
    let startTime: Date
    var endTime: Date?
    
    // Balance
    let startingBalance: Int
    var endingBalance: Int
    
    // Common statistics (all games track these)
    var roundsPlayed: Int
    var wins: Int
    var losses: Int
    var pushes: Int
    var totalWinnings: Int
    var biggestWin: Int
    var biggestLoss: Int
    var totalBetAmount: Int
    var biggestBet: Int
    
    // Game-specific statistics
    var gameStats: Stats
    
    // Computed
    var isActive: Bool
    var duration: TimeInterval
    var netResult: Int
    var winRate: Double
    var averageBet: Int
}

GameSpecificStats Protocol

Each game implements this to track game-specific statistics.

public protocol GameSpecificStats: Codable, Equatable, Sendable {
    init()
    var displayItems: [StatDisplayItem] { get }
}

Example: Blackjack Implementation

struct BlackjackStats: GameSpecificStats {
    var blackjacks: Int = 0
    var busts: Int = 0
    var surrenders: Int = 0
    var doubles: Int = 0
    var splits: Int = 0
    var insuranceTaken: Int = 0
    var insuranceWon: Int = 0
    
    var displayItems: [StatDisplayItem] {
        [
            StatDisplayItem(icon: "21.circle.fill", iconColor: .yellow, 
                          label: "Blackjacks", value: "\(blackjacks)"),
            StatDisplayItem(icon: "flame.fill", iconColor: .orange,
                          label: "Busts", value: "\(busts)"),
            // ... more items
        ]
    }
}

typealias BlackjackSession = GameSession<BlackjackStats>

Example: Baccarat Implementation

struct BaccaratStats: GameSpecificStats {
    var naturals: Int = 0
    var bankerWins: Int = 0
    var playerWins: Int = 0
    var ties: Int = 0
    var playerPairs: Int = 0
    var bankerPairs: Int = 0
    
    var displayItems: [StatDisplayItem] {
        [
            StatDisplayItem(icon: "sparkles", iconColor: .yellow,
                          label: "Naturals", value: "\(naturals)"),
            // ... more items
        ]
    }
}

typealias BaccaratSession = GameSession<BaccaratStats>

SessionManagedGame Protocol

Game state classes conform to this to get session management.

@MainActor
public protocol SessionManagedGame: AnyObject {
    associatedtype Stats: GameSpecificStats
    
    var currentSession: GameSession<Stats>? { get set }
    var sessionHistory: [GameSession<Stats>] { get set }
    var balance: Int { get set }
    var startingBalance: Int { get }
    var currentGameStyle: String { get }
    
    func saveGameData()
    func resetForNewSession()
}

Default implementations provided:

  • ensureActiveSession() - Create session if needed
  • startNewSession() - Start a fresh session
  • endCurrentSession(reason:) - End and archive session
  • endSessionAndStartNew() - End current and start new
  • handleSessionGameOver() - Handle running out of money
  • recordSessionRound(...) - Record a round result
  • aggregatedStats - Get combined stats
  • sessions(forStyle:) - Filter by game style

Integration Guide

Step 1: Create Game-Specific Stats

// In your game's Models folder
struct MyGameStats: GameSpecificStats {
    var customStat1: Int = 0
    var customStat2: Int = 0
    
    init() {}
    
    var displayItems: [StatDisplayItem] {
        [
            StatDisplayItem(
                icon: "star.fill",
                iconColor: .yellow,
                label: "Custom Stat 1",
                value: "\(customStat1)"
            ),
            // Add more as needed
        ]
    }
}

typealias MyGameSession = GameSession<MyGameStats>

Step 2: Update Game Data for Persistence

struct MyGameData: PersistableGameData, SessionPersistable {
    var currentSession: MyGameSession?
    var sessionHistory: [MyGameSession]
    var balance: Int
    // ... other data
}

Step 3: Conform GameState to SessionManagedGame

@Observable
@MainActor
final class GameState: SessionManagedGame {
    typealias Stats = MyGameStats
    
    var currentSession: MyGameSession?
    var sessionHistory: [MyGameSession] = []
    var balance: Int
    
    var startingBalance: Int { settings.startingBalance }
    var currentGameStyle: String { settings.gameStyle.rawValue }
    
    func saveGameData() {
        // Persist to CloudSyncManager
    }
    
    func resetForNewSession() {
        // Reset game-specific state (reshuffle deck, clear history, etc.)
    }
    
    init() {
        // Load saved data...
        ensureActiveSession()
    }
}

Step 4: Record Rounds

// At the end of each round:
recordSessionRound(
    winnings: roundWinnings,
    betAmount: betAmount,
    outcome: .win // or .lose, .push
) { stats in
    // Update game-specific stats
    if wasSpecialOutcome {
        stats.customStat1 += 1
    }
}

Step 5: Add Aggregation Extension (Optional)

extension Array where Element == MyGameSession {
    func aggregatedGameStats() -> MyGameStats {
        var combined = MyGameStats()
        for session in self {
            combined.customStat1 += session.gameStats.customStat1
            combined.customStat2 += session.gameStats.customStat2
        }
        return combined
    }
}

UI Components

CasinoKit provides reusable UI components:

EndSessionButton

A styled button to trigger ending a session.

EndSessionButton {
    state.showEndSessionConfirmation = true
}

EndSessionConfirmation

A confirmation dialog showing session summary.

EndSessionConfirmation(
    sessionDuration: session.duration,
    netResult: session.netResult,
    onConfirm: { state.endSessionAndStartNew() },
    onCancel: { dismiss() }
)

CurrentSessionHeader

Header showing active session with end button.

CurrentSessionHeader(
    duration: session.duration,
    roundsPlayed: session.roundsPlayed,
    netResult: session.netResult,
    onEndSession: { /* show confirmation */ }
)

SessionSummaryRow

A row for displaying a session in a list.

SessionSummaryRow(
    styleDisplayName: "Vegas Strip",
    duration: session.duration,
    roundsPlayed: session.roundsPlayed,
    netResult: session.netResult,
    startTime: session.startTime,
    isActive: session.isActive,
    endReason: session.endReason
)

GameStatRow

Display a single stat item from displayItems.

ForEach(session.gameStats.displayItems) { item in
    GameStatRow(item: item)
}

SessionFormatter

Utility for formatting session data:

SessionFormatter.formatDuration(seconds)      // "02h 15min"
SessionFormatter.formatMoney(amount)          // "$500" or "-$250"
SessionFormatter.formatPercent(value)         // "65.5%"
SessionFormatter.formatSessionDate(date)      // "Dec 29, 2024, 10:30 AM"
SessionFormatter.formatRelativeDate(date)     // "2h ago"

Session Flow

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  App Launch │────▶│ Load Data   │────▶│ Ensure      │
│             │     │             │     │ Session     │
└─────────────┘     └─────────────┘     └──────┬──────┘
                                               │
                    ┌──────────────────────────┘
                    ▼
┌─────────────────────────────────────────────────────┐
│                  ACTIVE SESSION                      │
│  ┌─────────┐     ┌─────────┐     ┌─────────┐       │
│  │  Bet    │────▶│  Play   │────▶│ Record  │──┐    │
│  │         │     │  Round  │     │ Round   │  │    │
│  └─────────┘     └─────────┘     └─────────┘  │    │
│       ▲                                       │    │
│       └───────────────────────────────────────┘    │
└────────────────────────┬───────────────────────────┘
                         │
         ┌───────────────┼───────────────┐
         ▼               ▼               ▼
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│ End Session │   │ Game Over   │   │ App Close   │
│ (Manual)    │   │ (Broke)     │   │ (Auto-save) │
└──────┬──────┘   └──────┬──────┘   └─────────────┘
       │                 │
       └────────┬────────┘
                ▼
┌─────────────────────────────────────────────────────┐
│  Session archived to history                         │
│  New session can be started                          │
└─────────────────────────────────────────────────────┘

Best Practices

  1. Always call ensureActiveSession() on init - Guarantees a session exists.

  2. Update balance in session after each round - The recordSessionRound method handles this.

  3. Implement resetForNewSession() properly - Clear round history, reshuffle decks, reset UI state.

  4. Use type aliases for convenience - typealias BlackjackSession = GameSession<BlackjackStats>

  5. Provide aggregation extensions - For combining game-specific stats across sessions.

  6. Use StatDisplayItem for consistent UI - Makes stats display automatic in UI components.

Data Storage

Session data is stored via CloudSyncManager which handles:

  • Local persistence to UserDefaults
  • iCloud sync when available
  • Conflict resolution using lastModified timestamps

The SessionPersistable protocol extends PersistableGameData to add:

public protocol SessionPersistable {
    associatedtype Stats: GameSpecificStats
    var currentSession: GameSession<Stats>? { get set }
    var sessionHistory: [GameSession<Stats>] { get set }
}