Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-25 09:51:40 -06:00
parent a470d8984c
commit 2c5b264f9b

View File

@ -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))")