// // GameState.swift // Baccarat // // Observable game state managing the flow of a baccarat game. // import Foundation import SwiftUI /// The current phase of a baccarat round. enum GamePhase: Equatable { case betting case dealingInitial case playerThirdCard case bankerThirdCard case showingResult case roundComplete } /// Main observable game state class managing all game logic and UI state. @Observable @MainActor final class GameState { // MARK: - Settings let settings: GameSettings // MARK: - Game Engine private(set) var engine: BaccaratEngine // MARK: - Player State var balance: Int = 10_000 var currentBets: [Bet] = [] // MARK: - Round State var currentPhase: GamePhase = .betting var lastResult: GameResult? var lastWinnings: Int = 0 // MARK: - Card Display State (for animations) var visiblePlayerCards: [Card] = [] var visibleBankerCards: [Card] = [] var playerCardsFaceUp: [Bool] = [] var bankerCardsFaceUp: [Bool] = [] // MARK: - History var roundHistory: [RoundResult] = [] // MARK: - Animation Flags var isAnimating: Bool = false var showResultBanner: Bool = false // MARK: - Computed Properties var totalBetAmount: Int { currentBets.reduce(0) { $0 + $1.amount } } var canPlaceBet: Bool { currentPhase == .betting && !isAnimating } var canDeal: Bool { currentPhase == .betting && hasMainBet && mainBetMeetsMinimum && !isAnimating } var playerHandValue: Int { engine.playerHand.value } var bankerHandValue: Int { engine.bankerHand.value } // Recent results for the road display (last 20) var recentResults: [RoundResult] { Array(roundHistory.suffix(20)) } // MARK: - Animation Timing (based on settings) private var dealDelay: Duration { .milliseconds(Int(400 * settings.dealingSpeed)) } private var flipDelay: Duration { .milliseconds(Int(300 * settings.dealingSpeed)) } private var shortDelay: Duration { .milliseconds(Int(200 * settings.dealingSpeed)) } private var resultDelay: Duration { .milliseconds(Int(500 * settings.dealingSpeed)) } // MARK: - Initialization /// Convenience initializer that constructs default settings on the main actor. convenience init() { self.init(settings: GameSettings()) } init(settings: GameSettings) { self.settings = settings self.engine = BaccaratEngine(deckCount: settings.deckCount.rawValue) self.balance = settings.startingBalance } // MARK: - Computed Properties for Bets /// Returns the current main bet (Player or Banker), if any. var mainBet: Bet? { currentBets.first(where: { $0.type == .player || $0.type == .banker }) } /// Returns the current tie bet, if any. var tieBet: Bet? { currentBets.first(where: { $0.type == .tie }) } /// Whether the player has placed a main bet (required to deal). var hasMainBet: Bool { mainBet != nil } /// Minimum bet for the table. var minBet: Int { settings.minBet } /// Maximum bet for the table. var maxBet: Int { settings.maxBet } /// Returns the current bet amount for a specific bet type. func betAmount(for type: BetType) -> Int { currentBets.first(where: { $0.type == type })?.amount ?? 0 } /// Whether the main bet meets the minimum requirement. var mainBetMeetsMinimum: Bool { guard let bet = mainBet else { return false } return bet.amount >= minBet } /// Whether a bet type can accept more chips (hasn't hit max). func canAddToBet(type: BetType, amount: Int) -> Bool { let currentAmount = betAmount(for: type) return currentAmount + amount <= maxBet } // MARK: - Betting Actions /// Places a bet of the specified amount on the given bet type. /// Player and Banker are mutually exclusive. Tie can be added as a side bet. /// Enforces min/max table limits. func placeBet(type: BetType, amount: Int) { guard canPlaceBet, balance >= amount else { return } // Check if adding this bet would exceed max let currentAmount = betAmount(for: type) guard currentAmount + amount <= maxBet else { return } // Handle mutually exclusive Player/Banker bets if type == .player || type == .banker { // Remove any existing opposite main bet and refund it if let existingMainBet = mainBet, existingMainBet.type != type { balance += existingMainBet.amount currentBets.removeAll(where: { $0.type == existingMainBet.type }) } } // Check if there's already a bet of this type if let index = currentBets.firstIndex(where: { $0.type == type }) { // Add to existing bet let existingBet = currentBets[index] currentBets[index] = Bet(type: type, amount: existingBet.amount + amount) } else { currentBets.append(Bet(type: type, amount: amount)) } balance -= amount } /// Clears all current bets and returns the amounts to balance. func clearBets() { guard canPlaceBet else { return } balance += totalBetAmount currentBets = [] } /// Removes the last bet placed. func undoLastBet() { guard canPlaceBet, let lastBet = currentBets.last else { return } balance += lastBet.amount currentBets.removeLast() } // MARK: - Game Flow /// Starts a new round by dealing cards with animation. func deal() async { guard canDeal else { return } isAnimating = true engine.prepareNewRound() // Clear visible cards visiblePlayerCards = [] visibleBankerCards = [] playerCardsFaceUp = [] bankerCardsFaceUp = [] lastResult = nil showResultBanner = false // Deal initial cards currentPhase = .dealingInitial let initialCards = engine.dealInitialCards() // Check if animations are enabled if settings.showAnimations { // Animate dealing: P1, B1, P2, B2 for (index, card) in initialCards.enumerated() { try? await Task.sleep(for: dealDelay) if index % 2 == 0 { visiblePlayerCards.append(card) playerCardsFaceUp.append(false) } else { visibleBankerCards.append(card) bankerCardsFaceUp.append(false) } } // Brief pause then flip cards try? await Task.sleep(for: flipDelay) // Flip all cards face up for i in 0..= 0 { balance += bet.amount } // Add winnings if payout > 0 { balance += payout } } lastWinnings = totalWinnings // Record result in history roundHistory.append(RoundResult( result: result, playerValue: playerHandValue, bankerValue: bankerHandValue )) // Show result banner showResultBanner = true try? await Task.sleep(for: .seconds(2)) currentPhase = .roundComplete showResultBanner = false isAnimating = false } /// Prepares for a new round. func newRound() { guard currentPhase == .roundComplete else { return } currentBets = [] visiblePlayerCards = [] visibleBankerCards = [] playerCardsFaceUp = [] bankerCardsFaceUp = [] lastResult = nil lastWinnings = 0 currentPhase = .betting } /// Rebet the same amounts as last round. func rebet() { guard currentPhase == .roundComplete || (currentPhase == .betting && currentBets.isEmpty) else { return } if currentPhase == .roundComplete { newRound() } } /// Resets the game to initial state with current settings. func resetGame() { engine = BaccaratEngine(deckCount: settings.deckCount.rawValue) balance = settings.startingBalance currentBets = [] currentPhase = .betting lastResult = nil lastWinnings = 0 visiblePlayerCards = [] visibleBankerCards = [] playerCardsFaceUp = [] bankerCardsFaceUp = [] roundHistory = [] isAnimating = false showResultBanner = false } /// Applies new settings (call after settings change). func applySettings() { resetGame() } }