// // GameState.swift // Blackjack // // Manages the game state machine for Blackjack. // import SwiftUI import CasinoKit /// Current phase of the game. enum GamePhase: Equatable { case betting case dealing case insurance case playerTurn(handIndex: Int) case dealerTurn case roundComplete } /// Main game state manager. @Observable @MainActor final class GameState { // MARK: - Core State /// Current player balance. private(set) var balance: Int /// Current game phase. private(set) var currentPhase: GamePhase = .betting /// The current bet amount (before deal). var currentBet: Int = 0 /// Insurance bet amount. var insuranceBet: Int = 0 // MARK: - Hands /// Player's hands (can have multiple after splits). private(set) var playerHands: [BlackjackHand] = [] /// Dealer's hand. private(set) var dealerHand: BlackjackHand = BlackjackHand() /// Index of the hand currently being played. private(set) var activeHandIndex: Int = 0 /// The active player hand. var activeHand: BlackjackHand? { guard activeHandIndex < playerHands.count else { return nil } return playerHands[activeHandIndex] } /// Dealer's face-up card. var dealerUpCard: Card? { dealerHand.cards.first } // MARK: - UI State /// Whether to show the result banner. var showResultBanner: Bool = false /// The result of the last round. private(set) var lastRoundResult: RoundResult? /// Round history for statistics. private(set) var roundHistory: [RoundResult] = [] // MARK: - Statistics (persisted) private(set) var totalWinnings: Int = 0 private(set) var biggestWin: Int = 0 private(set) var biggestLoss: Int = 0 private(set) var blackjackCount: Int = 0 private(set) var bustCount: Int = 0 // MARK: - Persistence /// iCloud sync manager for game data. let persistence: CloudSyncManager // MARK: - Engine & Settings /// The game engine. let engine: BlackjackEngine /// Game settings. let settings: GameSettings /// Sound manager. private let sound = SoundManager.shared // MARK: - Computed Properties /// Total bet across all hands. var totalBet: Int { playerHands.reduce(0) { $0 + $1.bet * ($1.isDoubledDown ? 2 : 1) } + insuranceBet } /// Whether player can place a bet. var canBet: Bool { currentPhase == .betting && currentBet + settings.minBet <= balance } /// Whether the current hand can hit. var canHit: Bool { guard case .playerTurn = currentPhase else { return false } return activeHand?.canHit ?? false } /// Whether the current hand can stand. var canStand: Bool { guard case .playerTurn = currentPhase else { return false } return !(activeHand?.isBusted ?? true) } /// Whether the current hand can double. var canDouble: Bool { guard case .playerTurn = currentPhase else { return false } guard let hand = activeHand else { return false } return engine.canDoubleDown(hand: hand, balance: balance) } /// Whether the current hand can split. var canSplit: Bool { guard case .playerTurn = currentPhase else { return false } guard let hand = activeHand else { return false } let splitCount = playerHands.count - 1 return engine.canSplit(hand: hand, balance: balance, currentSplitCount: splitCount) } /// Whether the player can surrender. var canSurrender: Bool { guard case .playerTurn = currentPhase else { return false } guard let hand = activeHand else { return false } return engine.canSurrender(hand: hand) } /// Whether the game is over (out of money). var isGameOver: Bool { balance < settings.minBet && currentPhase == .betting } /// Total rounds played. var roundsPlayed: Int { roundHistory.count } // MARK: - Initialization init(settings: GameSettings) { self.settings = settings self.balance = settings.startingBalance self.engine = BlackjackEngine(settings: settings) self.persistence = CloudSyncManager() syncSoundSettings() loadSavedGame() } /// Syncs sound settings with SoundManager. private func syncSoundSettings() { sound.soundEnabled = settings.soundEnabled sound.hapticsEnabled = settings.hapticsEnabled sound.volume = settings.soundVolume } // MARK: - Persistence /// Loads saved game data from iCloud or local storage. private func loadSavedGame() { let data = persistence.load() self.balance = data.balance self.totalWinnings = data.totalWinnings self.biggestWin = data.biggestWin self.biggestLoss = data.biggestLoss self.blackjackCount = data.blackjackCount self.bustCount = data.bustCount // Set up callback for when iCloud data arrives later persistence.onCloudDataReceived = { [weak self] newData in guard let self else { return } self.balance = newData.balance self.totalWinnings = newData.totalWinnings self.biggestWin = newData.biggestWin self.biggestLoss = newData.biggestLoss self.blackjackCount = newData.blackjackCount self.bustCount = newData.bustCount } } /// Saves current game data to iCloud and local storage. private func saveGameData() { let savedRounds: [SavedRoundResult] = roundHistory.map { result in SavedRoundResult( date: Date(), mainResult: result.mainHandResult.saveName, hadSplit: result.splitHandResult != nil, totalWinnings: result.totalWinnings ) } let data = BlackjackGameData( lastModified: Date(), balance: balance, roundHistory: savedRounds, totalWinnings: totalWinnings, biggestWin: biggestWin, biggestLoss: biggestLoss, blackjackCount: blackjackCount, bustCount: bustCount ) persistence.save(data) } /// Clears all saved data. func clearAllData() { persistence.reset() balance = settings.startingBalance totalWinnings = 0 biggestWin = 0 biggestLoss = 0 blackjackCount = 0 bustCount = 0 roundHistory = [] newRound() } // MARK: - Betting /// Places a bet. func placeBet(amount: Int) { guard canBet else { return } guard currentBet + amount <= settings.maxBet else { return } guard balance >= amount else { return } currentBet += amount balance -= amount sound.play(.chipPlace) } /// Clears the current bet. func clearBet() { balance += currentBet currentBet = 0 sound.play(.chipPlace) } // MARK: - Dealing /// Deals the initial cards. func deal() async { guard currentBet >= settings.minBet else { return } currentPhase = .dealing playerHands = [BlackjackHand(bet: currentBet)] dealerHand = BlackjackHand() activeHandIndex = 0 insuranceBet = 0 let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0 // Deal cards: player, dealer, player, dealer for i in 0..<4 { if let card = engine.dealCard() { if i % 2 == 0 { playerHands[0].cards.append(card) } else { dealerHand.cards.append(card) } sound.play(.cardDeal) if delay > 0 { try? await Task.sleep(for: .seconds(delay)) } } } // Check for insurance offer if let upCard = dealerUpCard, engine.shouldOfferInsurance(dealerUpCard: upCard) { currentPhase = .insurance return } // Check for immediate blackjacks await checkForBlackjacks() } /// Checks for blackjacks and handles accordingly. private func checkForBlackjacks() async { let playerBJ = playerHands[0].isBlackjack let dealerBJ = dealerHand.isBlackjack if playerBJ || dealerBJ { // Reveal dealer card sound.play(.cardFlip) if playerBJ && dealerBJ { // Push playerHands[0].result = .push await completeRound() } else if playerBJ { // Player wins playerHands[0].result = .blackjack await completeRound() } else { // Dealer wins playerHands[0].result = .lose await completeRound() } } else { currentPhase = .playerTurn(handIndex: 0) } } // MARK: - Insurance /// Takes insurance bet. func takeInsurance() async { let insuranceAmount = currentBet / 2 guard balance >= insuranceAmount else { declineInsurance() return } insuranceBet = insuranceAmount balance -= insuranceAmount sound.play(.chipPlace) // Check dealer blackjack if dealerHand.isBlackjack { sound.play(.cardFlip) // Insurance wins let payout = insuranceBet * 3 balance += payout playerHands[0].result = .lose await completeRound() } else { // Insurance loses, continue game insuranceBet = 0 // Lost the insurance bet await checkForBlackjacks() } } /// Declines insurance. func declineInsurance() { Task { await checkForBlackjacks() } } // MARK: - Player Actions /// Player hits (takes another card). func hit() async { guard canHit else { return } guard let card = engine.dealCard() else { return } playerHands[activeHandIndex].cards.append(card) sound.play(.cardDeal) // Check for bust or 21 if playerHands[activeHandIndex].isBusted { playerHands[activeHandIndex].result = .bust await moveToNextHand() } else if playerHands[activeHandIndex].value == 21 { playerHands[activeHandIndex].isStanding = true await moveToNextHand() } } /// Player stands. func stand() async { guard canStand else { return } playerHands[activeHandIndex].isStanding = true await moveToNextHand() } /// Player doubles down. func doubleDown() async { guard canDouble else { return } let additionalBet = playerHands[activeHandIndex].bet balance -= additionalBet playerHands[activeHandIndex].isDoubledDown = true sound.play(.chipPlace) // Deal one card and stand if let card = engine.dealCard() { playerHands[activeHandIndex].cards.append(card) sound.play(.cardDeal) } if playerHands[activeHandIndex].isBusted { playerHands[activeHandIndex].result = .bust } else { playerHands[activeHandIndex].isStanding = true } await moveToNextHand() } /// Player splits the hand. func split() async { guard canSplit else { return } let originalHand = playerHands[activeHandIndex] let splitCard = originalHand.cards[1] // Create two new hands var hand1 = BlackjackHand(cards: [originalHand.cards[0]], bet: originalHand.bet) hand1.isSplit = true var hand2 = BlackjackHand(cards: [splitCard], bet: originalHand.bet) hand2.isSplit = true // Deduct bet for second hand balance -= originalHand.bet sound.play(.chipPlace) // Deal one card to each hand if let card1 = engine.dealCard() { hand1.cards.append(card1) sound.play(.cardDeal) } if let card2 = engine.dealCard() { hand2.cards.append(card2) sound.play(.cardDeal) } // Replace original with split hands playerHands.remove(at: activeHandIndex) playerHands.insert(hand1, at: activeHandIndex) playerHands.insert(hand2, at: activeHandIndex + 1) // If split aces, typically only one card each and stand if originalHand.cards[0].rank == .ace && !settings.resplitAces { playerHands[activeHandIndex].isStanding = true playerHands[activeHandIndex + 1].isStanding = true await moveToNextHand() } else { currentPhase = .playerTurn(handIndex: activeHandIndex) } } /// Player surrenders. func surrender() async { guard canSurrender else { return } playerHands[activeHandIndex].result = .surrender await completeRound() } // MARK: - Hand Progression /// Moves to the next hand or dealer turn. private func moveToNextHand() async { // Check if there are more hands to play let nextIndex = activeHandIndex + 1 if nextIndex < playerHands.count { if !playerHands[nextIndex].isStanding && !playerHands[nextIndex].isBusted { activeHandIndex = nextIndex currentPhase = .playerTurn(handIndex: nextIndex) return } } // Check if all hands are busted let allBusted = playerHands.allSatisfy { $0.isBusted } if allBusted { await completeRound() return } // Dealer's turn await dealerTurn() } // MARK: - Dealer Turn /// Plays out the dealer's hand. private func dealerTurn() async { currentPhase = .dealerTurn // Reveal hole card sound.play(.cardFlip) let delay = settings.showAnimations ? 0.5 * settings.dealingSpeed : 0 if delay > 0 { try? await Task.sleep(for: .seconds(delay)) } // Dealer draws while engine.dealerShouldHit(hand: dealerHand) { if let card = engine.dealCard() { dealerHand.cards.append(card) sound.play(.cardDeal) if delay > 0 { try? await Task.sleep(for: .seconds(delay)) } } } await completeRound() } // MARK: - Round Completion /// Completes the round and calculates payouts. private func completeRound() async { currentPhase = .roundComplete var roundWinnings = 0 var wasBlackjack = false var hadBust = false // Evaluate each hand for i in 0.. biggestWin { biggestWin = roundWinnings } if roundWinnings < biggestLoss { biggestLoss = roundWinnings } if wasBlackjack { blackjackCount += 1 } if hadBust { bustCount += 1 } // Create round result lastRoundResult = RoundResult( mainHandResult: playerHands[0].result ?? .lose, splitHandResult: playerHands.count > 1 ? playerHands[1].result : nil, insuranceResult: insuranceBet > 0 ? (dealerHand.isBlackjack ? .insuranceWin : .insuranceLose) : nil, totalWinnings: roundWinnings, wasBlackjack: wasBlackjack ) roundHistory.append(lastRoundResult!) // Save game data to iCloud saveGameData() // Play appropriate sound if roundWinnings > 0 { sound.play(.win) } else if roundWinnings < 0 { sound.play(.lose) } else { sound.play(.push) } // Reset bet for next round currentBet = 0 showResultBanner = true // Check if shoe needs reshuffling if engine.needsReshuffle { engine.reshuffle() } } // MARK: - New Round /// Starts a new round. func newRound() { playerHands = [] dealerHand = BlackjackHand() activeHandIndex = 0 insuranceBet = 0 showResultBanner = false lastRoundResult = nil currentPhase = .betting sound.play(.newRound) } // MARK: - Game Reset /// Resets the entire game (keeps statistics). func resetGame() { balance = settings.startingBalance roundHistory = [] engine.reshuffle() newRound() saveGameData() } }