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

277 lines
8.4 KiB
Swift

//
// SessionManager.swift
// CasinoKit
//
// Protocol and utilities for managing game sessions.
// Games conform to SessionManagedGame to get session management for free.
//
import Foundation
import SwiftUI
// MARK: - Session Managed Game Protocol
/// Protocol for game state classes that use session management.
/// Conforming to this protocol gives you automatic session tracking.
///
/// Example usage in GameState:
/// ```swift
/// @Observable
/// final class GameState: SessionManagedGame {
/// typealias Stats = BlackjackStats
/// var currentSession: GameSession<BlackjackStats>?
/// var sessionHistory: [GameSession<BlackjackStats>] = []
/// // ... rest of implementation
/// }
/// ```
@MainActor
public protocol SessionManagedGame: AnyObject {
/// The game-specific stats type.
associatedtype Stats: GameSpecificStats
/// The currently active session (nil if no session).
var currentSession: GameSession<Stats>? { get set }
/// History of completed sessions.
var sessionHistory: [GameSession<Stats>] { get set }
/// Current player balance.
var balance: Int { get set }
/// Starting balance for new sessions (from settings).
var startingBalance: Int { get }
/// Current game style identifier (e.g., "vegas", "european").
var currentGameStyle: String { get }
/// Whether the end session confirmation dialog should be shown.
/// Games should bind this to their confirmation dialog.
var showEndSessionConfirmation: Bool { get set }
/// Called to persist game data after session changes.
func saveGameData()
/// Called when starting a new session to reset game-specific state.
func resetForNewSession()
/// Called before a session is deleted. Override to clean up associated data.
/// Default implementation does nothing.
func onWillDeleteSession(id: UUID)
/// Called before all session history is deleted. Override to clean up associated data.
/// Default implementation does nothing.
func onWillDeleteAllSessions()
}
// MARK: - Default Session Management Implementation
extension SessionManagedGame {
/// Ensures there's an active session, creating one if needed.
public func ensureActiveSession() {
if currentSession == nil {
startNewSession()
}
}
/// Starts a new session.
public func startNewSession() {
// End current session if exists
if currentSession != nil {
endCurrentSession(reason: .manualEnd)
}
// Create new session with current settings
currentSession = GameSession<Stats>(
gameStyle: currentGameStyle,
startingBalance: balance
)
saveGameData()
}
/// Ends the current session and adds it to history.
public func endCurrentSession(reason: SessionEndReason = .manualEnd) {
guard var session = currentSession else { return }
session.end(withBalance: balance)
// Add to history (most recent first)
sessionHistory.insert(session, at: 0)
currentSession = nil
saveGameData()
}
/// Ends the current session and starts a fresh one.
public func endSessionAndStartNew() {
endCurrentSession(reason: .manualEnd)
// Reset to starting balance
balance = startingBalance
// Let game reset its specific state
resetForNewSession()
// Start fresh session
startNewSession()
}
/// Called when player runs out of money.
public func handleSessionGameOver() {
endCurrentSession(reason: .brokeOut)
}
/// Records a round result in the current session.
/// Call this at the end of each round with the outcome.
public func recordSessionRound(
winnings: Int,
betAmount: Int,
outcome: RoundOutcome,
updateGameStats: ((inout Stats) -> Void)? = nil
) {
guard var session = currentSession else { return }
// Record common stats
session.recordRound(
winnings: winnings,
betAmount: betAmount,
outcome: outcome
)
// Let game update its specific stats
if let update = updateGameStats {
update(&session.gameStats)
}
// Update balance
session.updateBalance(balance)
currentSession = session
saveGameData()
}
// MARK: - Computed Helpers
/// Whether there's an active session.
public var hasActiveSession: Bool {
currentSession != nil
}
/// All sessions including current.
public var allSessions: [GameSession<Stats>] {
var sessions: [GameSession<Stats>] = []
if let current = currentSession {
sessions.append(current)
}
sessions.append(contentsOf: sessionHistory)
return sessions
}
/// Aggregated stats from all sessions.
public var aggregatedStats: AggregatedSessionStats {
allSessions.aggregatedStats()
}
/// Sessions filtered by game style.
public func sessions(forStyle style: String) -> [GameSession<Stats>] {
allSessions.filter { $0.gameStyle == style }
}
/// Aggregated stats for a specific style.
public func aggregatedStats(forStyle style: String) -> AggregatedSessionStats {
sessions(forStyle: style).aggregatedStats()
}
// MARK: - Session History Management
/// Default implementation - does nothing. Override to clean up session-specific data.
public func onWillDeleteSession(id: UUID) {
// Override in game to clean up associated data (e.g., round histories)
}
/// Default implementation - does nothing. Override to clean up all session data.
public func onWillDeleteAllSessions() {
// Override in game to clean up associated data (e.g., round histories)
}
/// Deletes a session from history by ID.
public func deleteSession(id: UUID) {
onWillDeleteSession(id: id)
sessionHistory.removeAll { $0.id == id }
saveGameData()
}
/// Deletes all session history.
public func deleteAllSessionHistory() {
onWillDeleteAllSessions()
sessionHistory.removeAll()
saveGameData()
}
}
// MARK: - Session Formatter
/// Utility for formatting session data for display.
public enum SessionFormatter {
/// Formats a duration as "XXh XXmin".
public static func formatDuration(_ seconds: TimeInterval) -> String {
let hours = Int(seconds) / 3600
let minutes = (Int(seconds) % 3600) / 60
return String(format: "%02dh %02dmin", hours, minutes)
}
/// Formats a duration as "X hours, Y minutes" for accessibility.
public static func formatDurationAccessible(_ seconds: TimeInterval) -> String {
let hours = Int(seconds) / 3600
let minutes = (Int(seconds) % 3600) / 60
if hours > 0 {
return "\(hours) hours, \(minutes) minutes"
} else {
return "\(minutes) minutes"
}
}
/// Formats money with sign.
public static func formatMoney(_ amount: Int) -> String {
if amount >= 0 {
return "$\(amount)"
} else {
return "-$\(abs(amount))"
}
}
/// Formats a date as relative time (e.g., "2 hours ago").
public static func formatRelativeDate(_ date: Date) -> String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
return formatter.localizedString(for: date, relativeTo: Date())
}
/// Formats a date for session display.
public static func formatSessionDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
/// Formats a percentage.
public static func formatPercent(_ value: Double) -> String {
value.formatted(.number.precision(.fractionLength(1))) + "%"
}
}
// MARK: - Session Data Protocol
/// Protocol for game data structs that store sessions.
/// Use this to define your PersistableGameData with session support.
public protocol SessionPersistable {
associatedtype Stats: GameSpecificStats
var currentSession: GameSession<Stats>? { get set }
var sessionHistory: [GameSession<Stats>] { get set }
}