766 lines
23 KiB
Swift
766 lines
23 KiB
Swift
//
|
|
// 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
|
|
|
|
/// Whether a reshuffle notification should be shown.
|
|
var showReshuffleNotification: Bool = false
|
|
|
|
// 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<BlackjackGameData>
|
|
|
|
// 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.
|
|
/// True if in betting phase, have balance, and haven't hit max bet.
|
|
var canBet: Bool {
|
|
currentPhase == .betting && balance > 0 && currentBet < settings.maxBet
|
|
}
|
|
|
|
/// 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 and no active bet).
|
|
var isGameOver: Bool {
|
|
balance < settings.minBet && currentPhase == .betting && currentBet == 0
|
|
}
|
|
|
|
/// Total rounds played.
|
|
var roundsPlayed: Int {
|
|
roundHistory.count
|
|
}
|
|
|
|
/// Whether it's currently the player's turn.
|
|
var isPlayerTurn: Bool {
|
|
if case .playerTurn = currentPhase { return true }
|
|
return false
|
|
}
|
|
|
|
/// Whether the dealer's hole card should be revealed.
|
|
var shouldShowDealerHoleCard: Bool {
|
|
switch currentPhase {
|
|
case .dealerTurn, .roundComplete:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Hints
|
|
|
|
/// Current gameplay hint based on basic strategy.
|
|
var currentHint: String? {
|
|
guard settings.showHints else { return nil }
|
|
guard isPlayerTurn else { return nil }
|
|
guard let hand = activeHand,
|
|
let upCard = dealerUpCard else { return nil }
|
|
|
|
// Use count-adjusted hints when card counting is enabled
|
|
if settings.showCardCount {
|
|
return engine.getCountAdjustedHint(playerHand: hand, dealerUpCard: upCard)
|
|
}
|
|
return engine.getHint(playerHand: hand, dealerUpCard: upCard)
|
|
}
|
|
|
|
/// Betting recommendation based on the true count.
|
|
var bettingHint: String? {
|
|
guard settings.showCardCount else { return nil }
|
|
guard currentPhase == .betting else { return nil }
|
|
|
|
let tc = Int(engine.trueCount.rounded())
|
|
|
|
switch tc {
|
|
case ...(-2):
|
|
return String(localized: "Bet minimum or sit out")
|
|
case -1:
|
|
return String(localized: "Bet minimum")
|
|
case 0:
|
|
return String(localized: "Bet minimum (neutral)")
|
|
case 1:
|
|
return String(localized: "Bet 2x minimum")
|
|
case 2:
|
|
return String(localized: "Bet 4x minimum")
|
|
case 3:
|
|
return String(localized: "Bet 6x minimum")
|
|
case 4:
|
|
return String(localized: "Bet 8x minimum")
|
|
case 5...:
|
|
return String(localized: "Bet maximum!")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(settings: GameSettings) {
|
|
self.settings = settings
|
|
self.balance = settings.startingBalance
|
|
self.engine = BlackjackEngine(settings: settings)
|
|
self.persistence = CloudSyncManager<BlackjackGameData>()
|
|
syncSoundSettings()
|
|
loadSavedGame()
|
|
}
|
|
|
|
/// Syncs sound settings with SoundManager.
|
|
private func syncSoundSettings() {
|
|
sound.soundEnabled = settings.soundEnabled
|
|
sound.hapticsEnabled = settings.hapticsEnabled
|
|
sound.volume = settings.soundVolume
|
|
}
|
|
|
|
/// Called when deck count setting changes - reshuffles with new deck count.
|
|
func applyDeckCountChange() {
|
|
engine.reshuffle()
|
|
}
|
|
|
|
// 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.hadSplit,
|
|
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
|
|
|
|
/// Whether the player can deal (betting phase with valid bet).
|
|
var canDeal: Bool {
|
|
currentPhase == .betting && currentBet >= settings.minBet
|
|
}
|
|
|
|
/// Deals the initial cards.
|
|
func deal() async {
|
|
guard canDeal else { return }
|
|
|
|
// Ensure enough cards for a full hand - reshuffle if needed
|
|
if !engine.canDealNewHand {
|
|
engine.reshuffle()
|
|
showReshuffleNotification = true
|
|
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(2))
|
|
showReshuffleNotification = false
|
|
}
|
|
}
|
|
|
|
currentPhase = .dealing
|
|
playerHands = [BlackjackHand(bet: currentBet)]
|
|
dealerHand = BlackjackHand()
|
|
activeHandIndex = 0
|
|
insuranceBet = 0
|
|
|
|
let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0
|
|
|
|
// European no-hole-card: deal 3 cards (player, dealer, player)
|
|
// American style: deal 4 cards (player, dealer, player, dealer)
|
|
let cardCount = settings.noHoleCard ? 3 : 4
|
|
|
|
for i in 0..<cardCount {
|
|
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 (only in American style with hole card)
|
|
if !settings.noHoleCard, let upCard = dealerUpCard, engine.shouldOfferInsurance(dealerUpCard: upCard) {
|
|
currentPhase = .insurance
|
|
return
|
|
}
|
|
|
|
// Check for immediate blackjacks (only in American style - European checks after player acts)
|
|
if !settings.noHoleCard {
|
|
await checkForBlackjacks()
|
|
} else {
|
|
// European: just go to player turn (blackjacks checked after player acts)
|
|
if playerHands[0].isBlackjack {
|
|
// Player blackjack - will be handled after dealer gets second card
|
|
currentPhase = .playerTurn(handIndex: 0)
|
|
} else {
|
|
currentPhase = .playerTurn(handIndex: 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
|
|
let delay = settings.showAnimations ? 0.5 * settings.dealingSpeed : 0
|
|
|
|
// European no-hole-card: deal the second card now
|
|
if settings.noHoleCard && dealerHand.cards.count == 1 {
|
|
if let card = engine.dealCard() {
|
|
dealerHand.cards.append(card)
|
|
sound.play(.cardDeal)
|
|
|
|
if delay > 0 {
|
|
try? await Task.sleep(for: .seconds(delay))
|
|
}
|
|
}
|
|
|
|
// Check for dealer blackjack in European mode
|
|
// Player loses everything (no early check in European)
|
|
if dealerHand.isBlackjack {
|
|
// Mark player hands as lost if they don't have blackjack
|
|
for i in 0..<playerHands.count {
|
|
if playerHands[i].result == nil {
|
|
if playerHands[i].isBlackjack {
|
|
playerHands[i].result = .push
|
|
} else {
|
|
playerHands[i].result = .lose
|
|
}
|
|
}
|
|
}
|
|
await completeRound()
|
|
return
|
|
}
|
|
} else {
|
|
// American style: reveal hole card
|
|
sound.play(.cardFlip)
|
|
|
|
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..<playerHands.count {
|
|
if playerHands[i].result == nil {
|
|
playerHands[i].result = engine.determineResult(
|
|
playerHand: playerHands[i],
|
|
dealerHand: dealerHand
|
|
)
|
|
}
|
|
|
|
if let result = playerHands[i].result {
|
|
let payout = engine.calculatePayout(
|
|
bet: playerHands[i].bet,
|
|
result: result,
|
|
isDoubled: playerHands[i].isDoubledDown
|
|
)
|
|
balance += payout
|
|
roundWinnings += payout - playerHands[i].bet * (playerHands[i].isDoubledDown ? 2 : 1)
|
|
|
|
if result == .blackjack {
|
|
wasBlackjack = true
|
|
}
|
|
if result == .bust {
|
|
hadBust = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update statistics
|
|
totalWinnings += roundWinnings
|
|
if roundWinnings > biggestWin {
|
|
biggestWin = roundWinnings
|
|
}
|
|
if roundWinnings < biggestLoss {
|
|
biggestLoss = roundWinnings
|
|
}
|
|
if wasBlackjack {
|
|
blackjackCount += 1
|
|
}
|
|
if hadBust {
|
|
bustCount += 1
|
|
}
|
|
|
|
// Create round result with all hand results
|
|
let allHandResults = playerHands.map { $0.result ?? .lose }
|
|
lastRoundResult = RoundResult(
|
|
handResults: allHandResults,
|
|
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()
|
|
showReshuffleNotification = true
|
|
|
|
// Auto-dismiss after a delay
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(2))
|
|
showReshuffleNotification = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - New Round
|
|
|
|
/// Starts a new round.
|
|
func newRound() {
|
|
// Reset all hand state
|
|
playerHands = []
|
|
dealerHand = BlackjackHand()
|
|
activeHandIndex = 0
|
|
|
|
// Reset bets
|
|
currentBet = 0
|
|
insuranceBet = 0
|
|
|
|
// Reset UI state
|
|
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()
|
|
}
|
|
}
|
|
|