393 lines
12 KiB
Swift
393 lines
12 KiB
Swift
//
|
|
// 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..<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
|
|
|
|
// Calculate and apply payouts
|
|
var totalWinnings = 0
|
|
for bet in currentBets {
|
|
let payout = engine.calculatePayout(bet: bet, result: result)
|
|
totalWinnings += payout
|
|
|
|
// Return original bet if not a loss
|
|
if payout >= 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()
|
|
}
|
|
}
|
|
|