// // BlackjackEngine.swift // Blackjack // // Core game logic for Blackjack. // import Foundation import CasinoKit /// Manages the Blackjack game rules and shoe. @Observable @MainActor final class BlackjackEngine { // MARK: - Properties /// The card shoe. private(set) var shoe: Deck /// Number of decks in the shoe. let deckCount: Int /// Settings reference for rule variations. private let settings: GameSettings /// Cards remaining in shoe. var cardsRemaining: Int { shoe.cardsRemaining } /// Whether the shoe needs reshuffling (below 25% remaining). var needsReshuffle: Bool { let threshold = (52 * deckCount) / 4 return cardsRemaining < threshold } // MARK: - Initialization init(settings: GameSettings) { self.settings = settings self.deckCount = settings.deckCount.rawValue self.shoe = Deck(deckCount: deckCount) shoe.shuffle() } // MARK: - Shoe Management /// Reshuffles the shoe. func reshuffle() { shoe = Deck(deckCount: deckCount) shoe.shuffle() } /// Deals a single card from the shoe. func dealCard() -> Card? { shoe.draw() } // MARK: - Hand Evaluation /// Determines if dealer should hit based on rules. func dealerShouldHit(hand: BlackjackHand) -> Bool { let value = hand.value if value < 17 { return true } if value == 17 && hand.isSoft && settings.dealerHitsSoft17 { return true } return false } /// Determines the result of a player hand against dealer. func determineResult(playerHand: BlackjackHand, dealerHand: BlackjackHand) -> HandResult { let playerValue = playerHand.value let dealerValue = dealerHand.value // Player busted if playerHand.isBusted { return .bust } // Player has blackjack if playerHand.isBlackjack { if dealerHand.isBlackjack { return .push } return .blackjack } // Dealer busted if dealerHand.isBusted { return .win } // Dealer has blackjack (player doesn't) if dealerHand.isBlackjack { return .lose } // Compare values if playerValue > dealerValue { return .win } else if playerValue < dealerValue { return .lose } else { return .push } } /// Calculates payout for a hand result. func calculatePayout(bet: Int, result: HandResult, isDoubled: Bool) -> Int { let effectiveBet = isDoubled ? bet * 2 : bet switch result { case .blackjack: return Int(Double(bet) * settings.blackjackPayout) + bet case .win: return effectiveBet * 2 case .push: return effectiveBet case .lose, .bust: return 0 case .surrender: return bet / 2 case .insuranceWin: return bet * 3 // 2:1 + original bet case .insuranceLose: return 0 } } // MARK: - Action Availability /// Whether player can double down on this hand. func canDoubleDown(hand: BlackjackHand, balance: Int) -> Bool { guard hand.cards.count == 2 else { return false } guard !hand.isDoubledDown else { return false } guard balance >= hand.bet else { return false } // After split, check DAS rule if hand.isSplit && !settings.doubleAfterSplit { return false } return true } /// Whether player can split this hand. func canSplit(hand: BlackjackHand, balance: Int, currentSplitCount: Int) -> Bool { guard hand.canSplit else { return false } guard balance >= hand.bet else { return false } guard currentSplitCount < 3 else { return false } // Max 4 hands // Check resplit aces if hand.isSplit && hand.cards.first?.rank == .ace && !settings.resplitAces { return false } return true } /// Whether player can surrender. func canSurrender(hand: BlackjackHand) -> Bool { guard settings.lateSurrender else { return false } guard hand.cards.count == 2 else { return false } guard !hand.isSplit else { return false } return true } /// Whether insurance should be offered. func shouldOfferInsurance(dealerUpCard: Card) -> Bool { settings.insuranceAllowed && dealerUpCard.rank == .ace } // MARK: - Basic Strategy Hint /// Returns the basic strategy recommendation. func getHint(playerHand: BlackjackHand, dealerUpCard: Card) -> String { let playerValue = playerHand.value let dealerValue = dealerUpCard.blackjackValue let isSoft = playerHand.isSoft let canDouble = playerHand.cards.count == 2 // Pairs if playerHand.canSplit { let pairRank = playerHand.cards[0].rank switch pairRank { case .ace, .eight: return String(localized: "Split") case .ten, .jack, .queen, .king: return String(localized: "Stand") case .five: return canDouble ? String(localized: "Double") : String(localized: "Hit") case .four: return (dealerValue == 5 || dealerValue == 6) ? String(localized: "Split") : String(localized: "Hit") case .two, .three, .seven: return dealerValue <= 7 ? String(localized: "Split") : String(localized: "Hit") case .six: return dealerValue <= 6 ? String(localized: "Split") : String(localized: "Hit") case .nine: return (dealerValue == 7 || dealerValue >= 10) ? String(localized: "Stand") : String(localized: "Split") } } // Soft hands if isSoft { if playerValue >= 19 { return String(localized: "Stand") } if playerValue == 18 { if dealerValue >= 9 { return String(localized: "Hit") } return String(localized: "Stand") } // Soft 17 or less return String(localized: "Hit") } // Hard hands if playerValue >= 17 { return String(localized: "Stand") } if playerValue >= 13 && dealerValue <= 6 { return String(localized: "Stand") } if playerValue == 12 && dealerValue >= 4 && dealerValue <= 6 { return String(localized: "Stand") } if playerValue == 11 && canDouble { return String(localized: "Double") } if playerValue == 10 && dealerValue <= 9 && canDouble { return String(localized: "Double") } if playerValue == 9 && dealerValue >= 3 && dealerValue <= 6 && canDouble { return String(localized: "Double") } return String(localized: "Hit") } }