From 2c5b264f9b476f445740affc0f8e51df146beb08 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 25 Dec 2025 09:51:40 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../Blackjack/Engine/BlackjackEngine.swift | 164 ++++++++++++------ 1 file changed, 115 insertions(+), 49 deletions(-) diff --git a/Blackjack/Blackjack/Engine/BlackjackEngine.swift b/Blackjack/Blackjack/Engine/BlackjackEngine.swift index e4eea6a..748c12c 100644 --- a/Blackjack/Blackjack/Engine/BlackjackEngine.swift +++ b/Blackjack/Blackjack/Engine/BlackjackEngine.swift @@ -248,20 +248,92 @@ final class BlackjackEngine { // MARK: - Basic Strategy Hint - /// Returns the basic strategy recommendation based on BJA chart. + /// Returns the basic strategy recommendation based on standard casino strategy cards. /// Accounts for game settings (surrender, dealer hits soft 17, etc.) + /// + /// Basic strategy is the mathematically optimal play for each hand combination. + /// This implementation follows the standard multi-deck basic strategy chart. func getHint(playerHand: BlackjackHand, dealerUpCard: Card) -> String { let playerValue = playerHand.value - let dealerValue = dealerUpCard.blackjackValue + let dealerRank = dealerUpCard.rank let isSoft = playerHand.isSoft let canDouble = playerHand.cards.count == 2 let surrenderAvailable = settings.lateSurrender let dealerHitsS17 = settings.dealerHitsSoft17 - // SURRENDER (when available) - check first - if surrenderAvailable && playerHand.cards.count == 2 { - // 16 vs 9, 10, A - Surrender - if playerValue == 16 && !isSoft && (dealerValue >= 9 || dealerValue == 1) { + // Helper: Convert dealer rank to numeric value (Ace = 11 for comparison) + let dealerValue: Int = { + switch dealerRank { + case .ace: return 11 + case .two: return 2 + case .three: return 3 + case .four: return 4 + case .five: return 5 + case .six: return 6 + case .seven: return 7 + case .eight: return 8 + case .nine: return 9 + case .ten, .jack, .queen, .king: return 10 + } + }() + + // PAIRS - Check first (before surrender, since splitting Aces/8s is always correct) + // This matches standard strategy cards: "Always split Aces and 8s" + if playerHand.canSplit { + let pairRank = playerHand.cards[0].rank + switch pairRank { + case .ace: + // ALWAYS split Aces - this is one of the most important rules + return String(localized: "Split") + case .eight: + // ALWAYS split 8s - two 8s (16) is the worst hand; two hands starting with 8 is better + return String(localized: "Split") + case .ten, .jack, .queen, .king: + // NEVER split 10s - 20 is too strong to break up + return String(localized: "Stand") + case .five: + // NEVER split 5s - treat as hard 10 and double when favorable + if canDouble && dealerValue >= 2 && dealerValue <= 9 { + return String(localized: "Double") + } + return String(localized: "Hit") + case .four: + // Split 4s only vs 5-6 when DAS allowed, otherwise hit + if settings.doubleAfterSplit && (dealerValue == 5 || dealerValue == 6) { + return String(localized: "Split") + } + return String(localized: "Hit") + case .two, .three: + // Split 2s/3s vs dealer 2-7 + if dealerValue >= 2 && dealerValue <= 7 { + return String(localized: "Split") + } + return String(localized: "Hit") + case .six: + // Split 6s vs dealer 2-6 + if dealerValue >= 2 && dealerValue <= 6 { + return String(localized: "Split") + } + return String(localized: "Hit") + case .seven: + // Split 7s vs dealer 2-7 + if dealerValue >= 2 && dealerValue <= 7 { + return String(localized: "Split") + } + return String(localized: "Hit") + case .nine: + // Split 9s vs 2-6 and 8-9. Stand vs 7, 10, Ace + if dealerValue == 7 || dealerValue == 10 || dealerRank == .ace { + return String(localized: "Stand") + } + return String(localized: "Split") + } + } + + // SURRENDER (when available) - check after pairs since splitting 8s beats surrendering 16 + if surrenderAvailable && playerHand.cards.count == 2 && !playerHand.isSplit { + // 16 vs 9, 10, A - Surrender (but NOT a pair of 8s - those should split) + if playerValue == 16 && !isSoft && (dealerValue >= 9 || dealerRank == .ace) { return String(localized: "Surrender") } // 15 vs 10 - Surrender @@ -269,46 +341,11 @@ final class BlackjackEngine { return String(localized: "Surrender") } // 15 vs A - Surrender (if dealer hits soft 17) - if playerValue == 15 && !isSoft && dealerValue == 1 && dealerHitsS17 { + if playerValue == 15 && !isSoft && dealerRank == .ace && dealerHitsS17 { return String(localized: "Surrender") } } - // PAIRS - if playerHand.canSplit { - let pairRank = playerHand.cards[0].rank - switch pairRank { - case .ace: - return String(localized: "Split") - case .eight: - return String(localized: "Split") - case .ten, .jack, .queen, .king: - return String(localized: "Stand") - case .five: - // Never split 5s - treat as hard 10 - return (canDouble && dealerValue <= 9) ? String(localized: "Double") : String(localized: "Hit") - case .four: - // Split 4s vs 5-6 (if DAS), otherwise hit - return (settings.doubleAfterSplit && (dealerValue == 5 || dealerValue == 6)) - ? String(localized: "Split") : String(localized: "Hit") - case .two, .three: - // Split 2s/3s vs 2-7 - return dealerValue >= 2 && dealerValue <= 7 ? String(localized: "Split") : String(localized: "Hit") - case .six: - // Split 6s vs 2-6 - return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Split") : String(localized: "Hit") - case .seven: - // Split 7s vs 2-7 - return dealerValue >= 2 && dealerValue <= 7 ? String(localized: "Split") : String(localized: "Hit") - case .nine: - // Split 9s vs 2-6, 8-9. Stand vs 7, 10, A - if dealerValue == 7 || dealerValue == 10 || dealerValue == 1 { - return String(localized: "Stand") - } - return String(localized: "Split") - } - } - // SOFT HANDS (Ace counted as 11) if isSoft { switch playerValue { @@ -396,20 +433,45 @@ final class BlackjackEngine { /// Returns the count-adjusted strategy recommendation with deviation explanation. /// Based on the "Illustrious 18" - the most valuable count-based deviations. + /// + /// These deviations are used by card counters to improve on basic strategy + /// when the true count indicates a significant advantage/disadvantage. func getCountAdjustedHint(playerHand: BlackjackHand, dealerUpCard: Card) -> String { let basicHint = getHint(playerHand: playerHand, dealerUpCard: dealerUpCard) let tc = Int(trueCount.rounded()) let playerValue = playerHand.value - let dealerValue = dealerUpCard.blackjackValue + let dealerRank = dealerUpCard.rank let isSoft = playerHand.isSoft + let canDouble = playerHand.cards.count == 2 + + // Helper: Convert dealer rank to numeric value (Ace = 11) + let dealerValue: Int = { + switch dealerRank { + case .ace: return 11 + case .two: return 2 + case .three: return 3 + case .four: return 4 + case .five: return 5 + case .six: return 6 + case .seven: return 7 + case .eight: return 8 + case .nine: return 9 + case .ten, .jack, .queen, .king: return 10 + } + }() // Helper to format true count with sign let tcDisplay = tc >= 0 ? "+\(tc)" : "\(tc)" // Check for count-based deviations from basic strategy (Illustrious 18) + // These are ordered by importance/frequency of occurrence + + // Note: Pair deviations ONLY apply when player can actually split + // Aces and 8s always split per basic strategy - no count deviation changes this // 16 vs 10: Stand when TC ≥ 0 (basic strategy says Hit) - if playerValue == 16 && !isSoft && dealerValue == 10 { + // This is one of the most important deviations + if playerValue == 16 && !isSoft && !playerHand.canSplit && dealerValue == 10 { if tc >= 0 { return String(localized: "Stand, not Hit (TC \(tcDisplay))") } @@ -422,6 +484,9 @@ final class BlackjackEngine { } } + // Insurance: Take when TC ≥ +3 (basic strategy says never take insurance) + // Note: This is handled in the betting phase, not here + // 12 vs 2: Stand when TC ≥ +3 (basic strategy says Hit) if playerValue == 12 && !isSoft && dealerValue == 2 { if tc >= 3 { @@ -451,41 +516,42 @@ final class BlackjackEngine { } // 16 vs 9: Stand when TC ≥ +5 (basic strategy says Hit) - if playerValue == 16 && !isSoft && dealerValue == 9 { + if playerValue == 16 && !isSoft && !playerHand.canSplit && dealerValue == 9 { if tc >= 5 { return String(localized: "Stand, not Hit (TC \(tcDisplay))") } } // 10 vs 10: Double when TC ≥ +4 (basic strategy says Hit) - if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 10 { + if playerValue == 10 && !isSoft && canDouble && dealerValue == 10 { if tc >= 4 { return String(localized: "Double, not Hit (TC \(tcDisplay))") } } // 10 vs Ace: Double when TC ≥ +4 (basic strategy says Hit) - if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 1 { + if playerValue == 10 && !isSoft && canDouble && dealerRank == .ace { if tc >= 4 { return String(localized: "Double, not Hit (TC \(tcDisplay))") } } // 9 vs 2: Double when TC ≥ +1 (basic strategy says Hit) - if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 2 { + if playerValue == 9 && !isSoft && canDouble && dealerValue == 2 { if tc >= 1 { return String(localized: "Double, not Hit (TC \(tcDisplay))") } } // 9 vs 7: Double when TC ≥ +3 (basic strategy says Hit) - if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 7 { + if playerValue == 9 && !isSoft && canDouble && dealerValue == 7 { if tc >= 3 { return String(localized: "Double, not Hit (TC \(tcDisplay))") } } // Pair of 10s vs 5: Split when TC ≥ +5 (basic strategy says Stand) + // This is an advanced play - only for high counts if playerHand.canSplit && playerHand.cards[0].blackjackValue == 10 && dealerValue == 5 { if tc >= 5 { return String(localized: "Split, not Stand (TC \(tcDisplay))")