329 lines
9.5 KiB
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
|
|
}
|
|
}
|