277 lines
8.4 KiB
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 }
|
|
}
|