248 lines
7.2 KiB
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")
|
|
}
|
|
}
|
|
|