410 lines
13 KiB
Markdown
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 }
|
|
}
|
|
```
|
|
|