// // GameSettings.swift // Baccarat // // User-configurable game settings. // import Foundation import SwiftUI import CasinoKit /// The number of decks available for the shoe. enum DeckCount: Int, CaseIterable, Identifiable { case one = 1 case six = 6 case eight = 8 var id: Int { rawValue } var displayName: String { switch self { case .one: return "1 Deck" case .six: return "6 Decks" case .eight: return "8 Decks (Standard)" } } var description: String { switch self { case .one: return "Rare, for private games" case .six: return "Common in mini baccarat" case .eight: return "Casino standard" } } } /// The style used to reveal cards. enum RevealStyle: String, CaseIterable, Codable, Identifiable { case auto // Automatically reveal (original behavior) case tap // Tap each card to reveal case squeeze // Tap + Hold / Drag for pressure-based reveal var id: String { rawValue } // Keep Identifiable conformance for SwiftUI var displayName: String { switch self { case .auto: return String(localized: "Auto Reveal") case .tap: return String(localized: "Tap Reveal") case .squeeze: return String(localized: "Squeeze Reveal") } } var helpText: String { switch self { case .auto: return String(localized: "Cards flip automatically") case .tap: return String(localized: "Tap each card to reveal") case .squeeze: return String(localized: "Squeeze card to peek at value") } } var iconName: String { switch self { case .auto: return "bolt.fill" case .tap: return "hand.tap.fill" case .squeeze: return "hand.point.up.left.and.text" } } } // TableLimits is now provided by CasinoKit /// Observable settings class for Baccarat configuration. /// Conforms to GameSettingsProtocol for shared settings behavior. @Observable @MainActor final class GameSettings: GameSettingsProtocol { // MARK: - Deck Settings /// Number of decks in the shoe. var deckCount: DeckCount = .eight // MARK: - Betting Limits /// The table limits preset. var tableLimits: TableLimits = .casual /// Minimum bet amount. var minBet: Int { tableLimits.minBet } /// Maximum bet amount per betting spot. var maxBet: Int { tableLimits.maxBet } // MARK: - Starting Balance /// The starting balance for new games. var startingBalance: Int = 1_000 // MARK: - Animation Settings /// Whether to show dealing animations. var showAnimations: Bool = true /// Speed of card dealing (uses CasinoDesign.DealingSpeed constants) var dealingSpeed: Double = CasinoDesign.DealingSpeed.normal /// The style used to reveal cards. var revealStyle: RevealStyle = .auto // MARK: - Display Settings /// Whether to show the cards remaining indicator. var showCardsRemaining: Bool = true /// Whether to show the history road map. var showHistory: Bool = true /// Whether to show betting hints and recommendations. var showHints: Bool = true // MARK: - Road Map Settings /// The preferred road type to display. var preferredRoadType: RoadType = .big /// Whether to show streak alerts. var showStreakAlerts: Bool = true // MARK: - Sound Settings /// Whether sound effects are enabled. var soundEnabled: Bool = true /// Whether haptic feedback is enabled. var hapticsEnabled: Bool = true /// Volume level for sound effects (0.0 to 1.0). var soundVolume: Float = 1.0 // MARK: - Persistence Keys private enum Keys { static let deckCount = "settings.deckCount" static let tableLimits = "settings.tableLimits" static let startingBalance = "settings.startingBalance" static let showAnimations = "settings.showAnimations" static let dealingSpeed = "settings.dealingSpeed" static let showCardsRemaining = "settings.showCardsRemaining" static let showHistory = "settings.showHistory" static let showHints = "settings.showHints" static let soundEnabled = "settings.soundEnabled" static let hapticsEnabled = "settings.hapticsEnabled" static let soundVolume = "settings.soundVolume" static let revealStyle = "settings.revealStyle" static let preferredRoadType = "settings.preferredRoadType" static let showStreakAlerts = "settings.showStreakAlerts" } // MARK: - iCloud /// Cached reference to iCloud store, lazily initialized private var _iCloudStore: NSUbiquitousKeyValueStore? private var _iCloudStoreInitialized = false private var iCloudStore: NSUbiquitousKeyValueStore? { // Return cached value if already attempted initialization if _iCloudStoreInitialized { return _iCloudStore } _iCloudStoreInitialized = true // Only access the store if iCloud is actually available guard iCloudAvailable else { return nil } _iCloudStore = NSUbiquitousKeyValueStore.default return _iCloudStore } /// Whether iCloud is available. var iCloudAvailable: Bool { // NSUbiquitousKeyValueStore only requires iCloud sign-in (token) // It does NOT require iCloud Drive/Documents to be enabled FileManager.default.ubiquityIdentityToken != nil } // MARK: - Initialization init() { load() // Register for iCloud changes (only if available) if iCloudAvailable, let store = iCloudStore { NotificationCenter.default.addObserver( forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: store, queue: .main ) { [weak self] _ in // Already on main queue, safe to call self?.loadFromiCloud() } // Trigger iCloud sync store.synchronize() } } // MARK: - Persistence /// Loads settings from UserDefaults, then checks iCloud for newer settings. func load() { // First load from local UserDefaults loadFromLocal() // Then check iCloud for potentially newer settings if iCloudAvailable { loadFromiCloud() } } /// Loads settings from local UserDefaults. private func loadFromLocal() { let defaults = UserDefaults.standard if let rawDeckCount = defaults.object(forKey: Keys.deckCount) as? Int, let deckCount = DeckCount(rawValue: rawDeckCount) { self.deckCount = deckCount } if let rawTableLimits = defaults.string(forKey: Keys.tableLimits), let tableLimits = TableLimits(rawValue: rawTableLimits) { self.tableLimits = tableLimits } if let balance = defaults.object(forKey: Keys.startingBalance) as? Int { self.startingBalance = Self.validatedStartingBalance(balance) } if defaults.object(forKey: Keys.showAnimations) != nil { self.showAnimations = defaults.bool(forKey: Keys.showAnimations) } if let speed = defaults.object(forKey: Keys.dealingSpeed) as? Double { self.dealingSpeed = Self.validatedDealingSpeed(speed) } if defaults.object(forKey: Keys.showCardsRemaining) != nil { self.showCardsRemaining = defaults.bool(forKey: Keys.showCardsRemaining) } if defaults.object(forKey: Keys.showHistory) != nil { self.showHistory = defaults.bool(forKey: Keys.showHistory) } if defaults.object(forKey: Keys.showHints) != nil { self.showHints = defaults.bool(forKey: Keys.showHints) } if defaults.object(forKey: Keys.soundEnabled) != nil { self.soundEnabled = defaults.bool(forKey: Keys.soundEnabled) } if defaults.object(forKey: Keys.hapticsEnabled) != nil { self.hapticsEnabled = defaults.bool(forKey: Keys.hapticsEnabled) } if let volume = defaults.object(forKey: Keys.soundVolume) as? Float { self.soundVolume = volume } if let rawStyle = defaults.string(forKey: Keys.revealStyle), let style = RevealStyle(rawValue: rawStyle) { self.revealStyle = style } if let rawRoadType = defaults.string(forKey: Keys.preferredRoadType), let roadType = RoadType(rawValue: rawRoadType) { self.preferredRoadType = roadType } if defaults.object(forKey: Keys.showStreakAlerts) != nil { self.showStreakAlerts = defaults.bool(forKey: Keys.showStreakAlerts) } } /// Loads settings from iCloud. private func loadFromiCloud() { guard iCloudAvailable, let store = iCloudStore else { return } if let rawDeckCount = store.object(forKey: Keys.deckCount) as? Int, let deckCount = DeckCount(rawValue: rawDeckCount) { self.deckCount = deckCount } if let rawTableLimits = store.string(forKey: Keys.tableLimits), let tableLimits = TableLimits(rawValue: rawTableLimits) { self.tableLimits = tableLimits } if let balance = store.object(forKey: Keys.startingBalance) as? Int { self.startingBalance = Self.validatedStartingBalance(balance) } if store.object(forKey: Keys.showAnimations) != nil { self.showAnimations = store.bool(forKey: Keys.showAnimations) } if let speed = store.object(forKey: Keys.dealingSpeed) as? Double { self.dealingSpeed = Self.validatedDealingSpeed(speed) } if store.object(forKey: Keys.showCardsRemaining) != nil { self.showCardsRemaining = store.bool(forKey: Keys.showCardsRemaining) } if store.object(forKey: Keys.showHistory) != nil { self.showHistory = store.bool(forKey: Keys.showHistory) } if store.object(forKey: Keys.showHints) != nil { self.showHints = store.bool(forKey: Keys.showHints) } if store.object(forKey: Keys.soundEnabled) != nil { self.soundEnabled = store.bool(forKey: Keys.soundEnabled) } if store.object(forKey: Keys.hapticsEnabled) != nil { self.hapticsEnabled = store.bool(forKey: Keys.hapticsEnabled) } if let volume = store.object(forKey: Keys.soundVolume) as? Double { self.soundVolume = Float(volume) } if let rawStyle = store.string(forKey: Keys.revealStyle), let style = RevealStyle(rawValue: rawStyle) { self.revealStyle = style } if let rawRoadType = store.string(forKey: Keys.preferredRoadType), let roadType = RoadType(rawValue: rawRoadType) { self.preferredRoadType = roadType } if store.object(forKey: Keys.showStreakAlerts) != nil { self.showStreakAlerts = store.bool(forKey: Keys.showStreakAlerts) } } /// Saves settings to UserDefaults and iCloud. func save() { // Save to local UserDefaults let defaults = UserDefaults.standard defaults.set(deckCount.rawValue, forKey: Keys.deckCount) defaults.set(tableLimits.rawValue, forKey: Keys.tableLimits) defaults.set(startingBalance, forKey: Keys.startingBalance) defaults.set(showAnimations, forKey: Keys.showAnimations) defaults.set(dealingSpeed, forKey: Keys.dealingSpeed) defaults.set(showCardsRemaining, forKey: Keys.showCardsRemaining) defaults.set(showHistory, forKey: Keys.showHistory) defaults.set(showHints, forKey: Keys.showHints) defaults.set(soundEnabled, forKey: Keys.soundEnabled) defaults.set(hapticsEnabled, forKey: Keys.hapticsEnabled) defaults.set(soundVolume, forKey: Keys.soundVolume) defaults.set(revealStyle.rawValue, forKey: Keys.revealStyle) defaults.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType) defaults.set(showStreakAlerts, forKey: Keys.showStreakAlerts) // Also save to iCloud if iCloudAvailable, let store = iCloudStore { store.set(deckCount.rawValue, forKey: Keys.deckCount) store.set(tableLimits.rawValue, forKey: Keys.tableLimits) store.set(startingBalance, forKey: Keys.startingBalance) store.set(showAnimations, forKey: Keys.showAnimations) store.set(dealingSpeed, forKey: Keys.dealingSpeed) store.set(showCardsRemaining, forKey: Keys.showCardsRemaining) store.set(showHistory, forKey: Keys.showHistory) store.set(showHints, forKey: Keys.showHints) store.set(soundEnabled, forKey: Keys.soundEnabled) store.set(hapticsEnabled, forKey: Keys.hapticsEnabled) store.set(Double(soundVolume), forKey: Keys.soundVolume) store.set(revealStyle.rawValue, forKey: Keys.revealStyle) store.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType) store.set(showStreakAlerts, forKey: Keys.showStreakAlerts) store.synchronize() } } /// Validates a dealing speed value, returning normal if invalid. /// This handles legacy data that may have been saved with incorrect values. private static func validatedDealingSpeed(_ speed: Double) -> Double { let validSpeeds = [ CasinoDesign.DealingSpeed.fast, CasinoDesign.DealingSpeed.normal, CasinoDesign.DealingSpeed.slow ] return validSpeeds.contains(speed) ? speed : CasinoDesign.DealingSpeed.normal } /// Validates a starting balance value, returning default if invalid. /// This handles legacy data that may have been saved with incorrect values. private static func validatedStartingBalance(_ balance: Int) -> Int { let validBalances = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000] return validBalances.contains(balance) ? balance : 1_000 } /// Resets all settings to defaults. func resetToDefaults() { deckCount = .eight tableLimits = .casual startingBalance = 1_000 showAnimations = true dealingSpeed = CasinoDesign.DealingSpeed.normal showCardsRemaining = true showHistory = true showHints = true soundEnabled = true hapticsEnabled = true soundVolume = 1.0 revealStyle = .auto preferredRoadType = .big showStreakAlerts = true save() } }