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

410 lines
13 KiB
Markdown

# 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<Stats>
A generic session that works with any game-specific stats type.
```swift
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.
```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<BlackjackStats>
```
**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<BaccaratStats>
```
### SessionManagedGame Protocol
Game state classes conform to this to get session management.
```swift
@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
```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<MyGameStats>
```
### 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<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:
```swift
public protocol SessionPersistable {
associatedtype Stats: GameSpecificStats
var currentSession: GameSession<Stats>? { get set }
var sessionHistory: [GameSession<Stats>] { get set }
}
```