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 neededstartNewSession()- Start a fresh sessionendCurrentSession(reason:)- End and archive sessionendSessionAndStartNew()- End current and start newhandleSessionGameOver()- Handle running out of moneyrecordSessionRound(...)- Record a round resultaggregatedStats- Get combined statssessions(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
-
Always call
ensureActiveSession()on init - Guarantees a session exists. -
Update balance in session after each round - The
recordSessionRoundmethod handles this. -
Implement
resetForNewSession()properly - Clear round history, reshuffle decks, reset UI state. -
Use type aliases for convenience -
typealias BlackjackSession = GameSession<BlackjackStats> -
Provide aggregation extensions - For combining game-specific stats across sessions.
-
Use
StatDisplayItemfor 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
lastModifiedtimestamps
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 }
}