# 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 - 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. ```swift public struct GameSession: 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. ```swift public protocol GameSpecificStats: Codable, Equatable, Sendable { init() var displayItems: [StatDisplayItem] { get } } ``` **Example: Blackjack Implementation** ```swift 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 ``` **Example: Baccarat Implementation** ```swift 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 ``` ### SessionManagedGame Protocol Game state classes conform to this to get session management. ```swift @MainActor public protocol SessionManagedGame: AnyObject { associatedtype Stats: GameSpecificStats var currentSession: GameSession? { get set } var sessionHistory: [GameSession] { 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 ```swift // 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 ``` ### Step 2: Update Game Data for Persistence ```swift struct MyGameData: PersistableGameData, SessionPersistable { var currentSession: MyGameSession? var sessionHistory: [MyGameSession] var balance: Int // ... other data } ``` ### Step 3: Conform GameState to SessionManagedGame ```swift @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 ```swift // 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) ```swift 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. ```swift EndSessionButton { state.showEndSessionConfirmation = true } ``` ### EndSessionConfirmation A confirmation dialog showing session summary. ```swift EndSessionConfirmation( sessionDuration: session.duration, netResult: session.netResult, onConfirm: { state.endSessionAndStartNew() }, onCancel: { dismiss() } ) ``` ### CurrentSessionHeader Header showing active session with end button. ```swift CurrentSessionHeader( duration: session.duration, roundsPlayed: session.roundsPlayed, netResult: session.netResult, onEndSession: { /* show confirmation */ } ) ``` ### SessionSummaryRow A row for displaying a session in a list. ```swift 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`. ```swift ForEach(session.gameStats.displayItems) { item in GameStatRow(item: item) } ``` ## SessionFormatter Utility for formatting session data: ```swift 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` 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: ```swift public protocol SessionPersistable { associatedtype Stats: GameSpecificStats var currentSession: GameSession? { get set } var sessionHistory: [GameSession] { get set } } ```