CasinoGames/Baccarat/Engine/GameState.swift

451 lines
13 KiB
Swift

//
// GameState.swift
// Baccarat
//
// Observable game state managing the flow of a baccarat game.
//
import Foundation
import SwiftUI
import CasinoKit
/// The current phase of a baccarat round.
enum GamePhase: Equatable {
case betting
case dealingInitial
case playerThirdCard
case bankerThirdCard
case showingResult
case roundComplete
}
/// Result of an individual bet after a round.
struct BetResult: Identifiable {
let id = UUID()
let type: BetType
let amount: Int
let payout: Int // Net winnings (positive) or loss (negative)
var isWin: Bool { payout > 0 }
var isLoss: Bool { payout < 0 }
var isPush: Bool { payout == 0 }
/// Display name for the bet type
var displayName: String {
switch type {
case .player: return "Player"
case .banker: return "Banker"
case .tie: return "Tie"
case .playerPair: return "P Pair"
case .bankerPair: return "B Pair"
case .dragonBonusPlayer: return "Dragon P"
case .dragonBonusBanker: return "Dragon B"
}
}
}
/// 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: - Bet Results
var playerHadPair: Bool = false
var bankerHadPair: Bool = false
var betResults: [BetResult] = []
// 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 })
}
/// Returns bets for a specific type.
func bet(for type: BetType) -> Bet? {
currentBets.first(where: { $0.type == type })
}
/// Whether the player has placed a main bet (required to deal).
var hasMainBet: Bool {
mainBet != nil
}
/// Whether the player has any side bets.
var hasSideBets: Bool {
currentBets.contains(where: { $0.type.isSideBet })
}
/// 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. Side bets can be added independently.
/// 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 = []
}
/// Undoes 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 and bet results
visiblePlayerCards = []
visibleBankerCards = []
playerCardsFaceUp = []
bankerCardsFaceUp = []
lastResult = nil
showResultBanner = false
playerHadPair = false
bankerHadPair = false
betResults = []
// 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..<playerCardsFaceUp.count {
playerCardsFaceUp[i] = true
}
for i in 0..<bankerCardsFaceUp.count {
bankerCardsFaceUp[i] = true
}
try? await Task.sleep(for: resultDelay)
} else {
// No animations - show all cards immediately
for (index, card) in initialCards.enumerated() {
if index % 2 == 0 {
visiblePlayerCards.append(card)
playerCardsFaceUp.append(true)
} else {
visibleBankerCards.append(card)
bankerCardsFaceUp.append(true)
}
}
}
// Check for naturals
if engine.playerHand.isNatural || engine.bankerHand.isNatural {
await showResult()
return
}
// Player third card
currentPhase = .playerThirdCard
if let playerThird = engine.drawPlayerThirdCard() {
if settings.showAnimations {
try? await Task.sleep(for: dealDelay)
visiblePlayerCards.append(playerThird)
playerCardsFaceUp.append(false)
try? await Task.sleep(for: shortDelay)
playerCardsFaceUp[2] = true
try? await Task.sleep(for: flipDelay)
} else {
visiblePlayerCards.append(playerThird)
playerCardsFaceUp.append(true)
}
}
// Banker third card
currentPhase = .bankerThirdCard
if let bankerThird = engine.drawBankerThirdCard() {
if settings.showAnimations {
try? await Task.sleep(for: dealDelay)
visibleBankerCards.append(bankerThird)
bankerCardsFaceUp.append(false)
try? await Task.sleep(for: shortDelay)
bankerCardsFaceUp[2] = true
try? await Task.sleep(for: dealDelay)
} else {
visibleBankerCards.append(bankerThird)
bankerCardsFaceUp.append(true)
}
}
await showResult()
}
/// Shows the result and processes payouts.
private func showResult() async {
currentPhase = .showingResult
let result = engine.determineResult()
lastResult = result
// Record pair results for display
playerHadPair = engine.playerHasPair
bankerHadPair = engine.bankerHasPair
// Calculate and apply payouts, track individual results
var totalWinnings = 0
var results: [BetResult] = []
for bet in currentBets {
let payout = engine.calculatePayout(bet: bet, result: result)
totalWinnings += payout
// Track individual bet result
results.append(BetResult(type: bet.type, amount: bet.amount, payout: payout))
// Return original bet if not a loss
if payout >= 0 {
balance += bet.amount
}
// Add winnings
if payout > 0 {
balance += payout
}
}
betResults = results
lastWinnings = totalWinnings
// Record result in history
roundHistory.append(RoundResult(
result: result,
playerValue: playerHandValue,
bankerValue: bankerHandValue
))
// Show result banner - stays until user taps New Round
showResultBanner = true
currentPhase = .roundComplete
isAnimating = false
}
/// Prepares for a new round.
func newRound() {
guard currentPhase == .roundComplete else { return }
// Dismiss result banner
showResultBanner = false
currentBets = []
visiblePlayerCards = []
visibleBankerCards = []
playerCardsFaceUp = []
bankerCardsFaceUp = []
lastResult = nil
lastWinnings = 0
playerHadPair = false
bankerHadPair = false
betResults = []
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
playerHadPair = false
bankerHadPair = false
betResults = []
}
/// Applies new settings (call after settings change).
func applySettings() {
resetGame()
}
}