CasinoGames/Blackjack/Engine/BlackjackEngine.swift

248 lines
7.2 KiB
Swift

//
// 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")
}
}