CasinoGames/CasinoKit/Sources/CasinoKit/Models/Session/GameSessionProtocol.swift

329 lines
9.5 KiB
Swift

//
// GameSessionProtocol.swift
// CasinoKit
//
// Generic protocols for game sessions that work with any casino game.
// Each game (Blackjack, Baccarat, etc.) provides their own implementation.
//
import Foundation
import SwiftUI
// MARK: - Game-Specific Stats Protocol
/// Protocol for game-specific statistics that each game implements.
/// Example: Blackjack tracks blackjacks, busts, surrenders.
/// Baccarat tracks naturals, banker wins, player wins.
public protocol GameSpecificStats: Codable, Equatable, Sendable {
/// Creates empty stats.
init()
/// A list of stat items to display in the UI.
/// Each game returns its specific stats in a displayable format.
var displayItems: [StatDisplayItem] { get }
}
/// A single stat item for display in the UI.
public struct StatDisplayItem: Identifiable, Sendable {
public let id = UUID()
public let icon: String
public let iconColor: Color
public let label: String
public let value: String
public let valueColor: Color
public init(
icon: String,
iconColor: Color = .white,
label: String,
value: String,
valueColor: Color = .white
) {
self.icon = icon
self.iconColor = iconColor
self.label = label
self.value = value
self.valueColor = valueColor
}
}
// MARK: - Round Outcome
/// Outcome of a single round - common to all casino games.
public enum RoundOutcome: String, Codable, Sendable {
case win
case lose
case push
}
// MARK: - Session End Reason
/// Reason why a session ended.
public enum SessionEndReason: String, Codable, Sendable {
case manualEnd = "ended" // Player chose to end session
case brokeOut = "broke" // Ran out of money
}
// MARK: - Game Session
/// A generic game session that works with any casino game.
/// The Stats type parameter allows each game to track game-specific statistics.
public struct GameSession<Stats: GameSpecificStats>: Codable, Identifiable, Equatable, Sendable {
// MARK: - Identity
/// Unique identifier for this session.
public let id: UUID
/// The game style/variant used during this session (e.g., "vegas", "european").
/// This is mutable to stay in sync with current settings.
public var gameStyle: String
// MARK: - Timing
/// When the session started.
public let startTime: Date
/// When the session ended (nil if still active).
public var endTime: Date?
// MARK: - Balance
/// Balance at the start of the session.
public let startingBalance: Int
/// Current/final balance for this session.
public var endingBalance: Int
// MARK: - Common Statistics (all games track these)
/// Number of rounds played in this session.
public var roundsPlayed: Int = 0
/// Number of winning rounds.
public var wins: Int = 0
/// Number of losing rounds.
public var losses: Int = 0
/// Number of pushed/tied rounds.
public var pushes: Int = 0
// MARK: - Financial Statistics
/// Net winnings/losses for this session.
public var totalWinnings: Int = 0
/// Biggest single round win.
public var biggestWin: Int = 0
/// Biggest single round loss (stored as negative).
public var biggestLoss: Int = 0
/// Total amount bet across all rounds.
public var totalBetAmount: Int = 0
/// Largest single bet placed.
public var biggestBet: Int = 0
// MARK: - Game-Specific Statistics
/// Game-specific statistics (Blackjack-specific, Baccarat-specific, etc.).
public var gameStats: Stats
// MARK: - Computed Properties
/// Whether this session is still active.
public var isActive: Bool {
endTime == nil
}
/// Duration of the session in seconds.
public var duration: TimeInterval {
let end = endTime ?? Date()
return end.timeIntervalSince(startTime)
}
/// Net result (ending balance - starting balance).
public var netResult: Int {
endingBalance - startingBalance
}
/// Win rate as a percentage (0-100).
public var winRate: Double {
guard roundsPlayed > 0 else { return 0 }
return Double(wins) / Double(roundsPlayed) * 100
}
/// Average bet per round.
public var averageBet: Int {
guard roundsPlayed > 0 else { return 0 }
return totalBetAmount / roundsPlayed
}
/// How the session ended.
public var endReason: SessionEndReason? {
guard !isActive else { return nil }
if endingBalance == 0 {
return .brokeOut
}
return .manualEnd
}
// MARK: - Initialization
/// Creates a new active session.
public init(gameStyle: String, startingBalance: Int) {
self.id = UUID()
self.gameStyle = gameStyle
self.startTime = Date()
self.endTime = nil
self.startingBalance = startingBalance
self.endingBalance = startingBalance
self.gameStats = Stats()
}
// MARK: - Recording
/// Records a round with the given outcome and updates stats.
public mutating func recordRound(
winnings: Int,
betAmount: Int,
outcome: RoundOutcome
) {
roundsPlayed += 1
totalWinnings += winnings
totalBetAmount += betAmount
if winnings > biggestWin {
biggestWin = winnings
}
if winnings < biggestLoss {
biggestLoss = winnings
}
if betAmount > biggestBet {
biggestBet = betAmount
}
switch outcome {
case .win:
wins += 1
case .lose:
losses += 1
case .push:
pushes += 1
}
}
/// Ends the session with the final balance.
public mutating func end(withBalance balance: Int) {
endTime = Date()
endingBalance = balance
}
/// Updates the ending balance (call after each round).
public mutating func updateBalance(_ balance: Int) {
endingBalance = balance
}
}
// MARK: - Aggregated Stats
/// Aggregated statistics across multiple sessions.
/// Works with any game type.
public struct AggregatedSessionStats: Sendable {
public var totalSessions: Int = 0
public var winningSessions: Int = 0
public var losingSessions: Int = 0
public var totalRoundsPlayed: Int = 0
public var totalWins: Int = 0
public var totalLosses: Int = 0
public var totalPushes: Int = 0
public var totalWinnings: Int = 0
public var biggestWin: Int = 0
public var biggestLoss: Int = 0
public var bestSession: Int = 0
public var worstSession: Int = 0
public var totalPlayTime: TimeInterval = 0
public var totalBetAmount: Int = 0
public var biggestBet: Int = 0
public var winRate: Double {
guard totalRoundsPlayed > 0 else { return 0 }
return Double(totalWins) / Double(totalRoundsPlayed) * 100
}
public var averageBet: Int {
guard totalRoundsPlayed > 0 else { return 0 }
return totalBetAmount / totalRoundsPlayed
}
public var sessionWinRate: Double {
guard totalSessions > 0 else { return 0 }
return Double(winningSessions) / Double(totalSessions) * 100
}
public init() {}
}
// MARK: - Array Extension for Aggregation
extension Array {
/// Aggregates sessions into combined statistics.
public func aggregatedStats<Stats: GameSpecificStats>() -> AggregatedSessionStats
where Element == GameSession<Stats> {
var stats = AggregatedSessionStats()
for session in self {
stats.totalSessions += 1
stats.totalRoundsPlayed += session.roundsPlayed
stats.totalWins += session.wins
stats.totalLosses += session.losses
stats.totalPushes += session.pushes
stats.totalWinnings += session.totalWinnings
stats.totalPlayTime += session.duration
stats.totalBetAmount += session.totalBetAmount
if session.biggestWin > stats.biggestWin {
stats.biggestWin = session.biggestWin
}
if session.biggestLoss < stats.biggestLoss {
stats.biggestLoss = session.biggestLoss
}
if session.biggestBet > stats.biggestBet {
stats.biggestBet = session.biggestBet
}
if session.netResult > 0 {
stats.winningSessions += 1
if session.netResult > stats.bestSession {
stats.bestSession = session.netResult
}
} else if session.netResult < 0 {
stats.losingSessions += 1
if session.netResult < stats.worstSession {
stats.worstSession = session.netResult
}
}
}
return stats
}
/// Aggregates game-specific stats from all sessions.
/// Games should provide their own aggregation logic.
public func aggregatedGameStats<Stats: GameSpecificStats>() -> Stats
where Element == GameSession<Stats> {
var combined = Stats()
// Game-specific stats need custom aggregation - this returns the last session's stats
// Each game should implement their own aggregation extension
if let last = self.last {
combined = last.gameStats
}
return combined
}
}