Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
13010a3131
commit
e455a5bda8
@ -20,6 +20,9 @@ final class BlackjackEngine {
|
|||||||
/// Settings reference for rule variations.
|
/// Settings reference for rule variations.
|
||||||
private let settings: GameSettings
|
private let settings: GameSettings
|
||||||
|
|
||||||
|
/// Running count for card counting (Hi-Lo system).
|
||||||
|
private(set) var runningCount: Int = 0
|
||||||
|
|
||||||
/// Number of decks in the shoe (reads from current settings).
|
/// Number of decks in the shoe (reads from current settings).
|
||||||
var deckCount: Int {
|
var deckCount: Int {
|
||||||
settings.deckCount.rawValue
|
settings.deckCount.rawValue
|
||||||
@ -30,10 +33,45 @@ final class BlackjackEngine {
|
|||||||
shoe.cardsRemaining
|
shoe.cardsRemaining
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the shoe needs reshuffling (below 25% remaining).
|
/// True count (running count / decks remaining).
|
||||||
|
var trueCount: Double {
|
||||||
|
let decksRemaining = max(1.0, Double(cardsRemaining) / 52.0)
|
||||||
|
return Double(runningCount) / decksRemaining
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimum cards needed to safely complete a hand.
|
||||||
|
/// Need ~10 cards worst case (player splits once, both hit twice, dealer hits twice).
|
||||||
|
private let minimumCardsForHand: Int = 10
|
||||||
|
|
||||||
|
/// The cut card position (cards to deal before reshuffling).
|
||||||
|
/// Set during reshuffle with slight random variation for realism.
|
||||||
|
private var cutCardPosition: Int = 0
|
||||||
|
|
||||||
|
/// Realistic default penetration based on deck count.
|
||||||
|
/// These match typical casino practices.
|
||||||
|
private var defaultPenetration: Double {
|
||||||
|
switch deckCount {
|
||||||
|
case 1: return 0.60 // 60% - common for single-deck (hand-held)
|
||||||
|
case 2: return 0.70 // 70% - typical double-deck pitch game
|
||||||
|
default: return 0.75 // 75% - standard for 4-8 deck shoe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the shoe needs reshuffling (hit cut card position).
|
||||||
var needsReshuffle: Bool {
|
var needsReshuffle: Bool {
|
||||||
let threshold = (52 * deckCount) / 4
|
let cardsDealt = (52 * deckCount) - cardsRemaining
|
||||||
return cardsRemaining < threshold
|
return cardsDealt >= cutCardPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether there are enough cards to start a new hand.
|
||||||
|
var canDealNewHand: Bool {
|
||||||
|
cardsRemaining >= minimumCardsForHand
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current penetration percentage (how much of the shoe has been dealt).
|
||||||
|
var penetrationPercentage: Double {
|
||||||
|
let totalCards = 52 * deckCount
|
||||||
|
return Double(totalCards - cardsRemaining) / Double(totalCards)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
@ -42,6 +80,10 @@ final class BlackjackEngine {
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.shoe = Deck(deckCount: settings.deckCount.rawValue)
|
self.shoe = Deck(deckCount: settings.deckCount.rawValue)
|
||||||
shoe.shuffle()
|
shoe.shuffle()
|
||||||
|
|
||||||
|
// Set initial cut card position
|
||||||
|
let totalCards = 52 * settings.deckCount.rawValue
|
||||||
|
cutCardPosition = Int(Double(totalCards) * defaultPenetration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Shoe Management
|
// MARK: - Shoe Management
|
||||||
@ -50,11 +92,38 @@ final class BlackjackEngine {
|
|||||||
func reshuffle() {
|
func reshuffle() {
|
||||||
shoe = Deck(deckCount: settings.deckCount.rawValue)
|
shoe = Deck(deckCount: settings.deckCount.rawValue)
|
||||||
shoe.shuffle()
|
shoe.shuffle()
|
||||||
|
runningCount = 0
|
||||||
|
|
||||||
|
// Calculate cut card position with slight random variation (±5%) for realism
|
||||||
|
let totalCards = 52 * deckCount
|
||||||
|
let basePenetration = defaultPenetration
|
||||||
|
let variation = Double.random(in: 0.95...1.05)
|
||||||
|
let cardsToDeal = Int(Double(totalCards) * basePenetration * variation)
|
||||||
|
|
||||||
|
// Clamp between 50% and 85% penetration
|
||||||
|
let minCards = Int(Double(totalCards) * 0.50)
|
||||||
|
let maxCards = Int(Double(totalCards) * 0.85)
|
||||||
|
cutCardPosition = max(min(cardsToDeal, maxCards), minCards)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deals a single card from the shoe.
|
/// Deals a single card from the shoe and updates the running count.
|
||||||
|
/// If the shoe is empty, automatically reshuffles and deals.
|
||||||
func dealCard() -> Card? {
|
func dealCard() -> Card? {
|
||||||
shoe.draw()
|
// Emergency reshuffle if shoe is empty (should rarely happen)
|
||||||
|
if cardsRemaining == 0 {
|
||||||
|
reshuffle()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let card = shoe.draw() else { return nil }
|
||||||
|
runningCount += card.hiLoValue
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Card Counting
|
||||||
|
|
||||||
|
/// Updates the running count when a card is revealed (e.g., dealer hole card).
|
||||||
|
func updateCount(for card: Card) {
|
||||||
|
runningCount += card.hiLoValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Hand Evaluation
|
// MARK: - Hand Evaluation
|
||||||
@ -179,70 +248,256 @@ final class BlackjackEngine {
|
|||||||
|
|
||||||
// MARK: - Basic Strategy Hint
|
// MARK: - Basic Strategy Hint
|
||||||
|
|
||||||
/// Returns the basic strategy recommendation.
|
/// Returns the basic strategy recommendation based on BJA chart.
|
||||||
|
/// Accounts for game settings (surrender, dealer hits soft 17, etc.)
|
||||||
func getHint(playerHand: BlackjackHand, dealerUpCard: Card) -> String {
|
func getHint(playerHand: BlackjackHand, dealerUpCard: Card) -> String {
|
||||||
let playerValue = playerHand.value
|
let playerValue = playerHand.value
|
||||||
let dealerValue = dealerUpCard.blackjackValue
|
let dealerValue = dealerUpCard.blackjackValue
|
||||||
let isSoft = playerHand.isSoft
|
let isSoft = playerHand.isSoft
|
||||||
let canDouble = playerHand.cards.count == 2
|
let canDouble = playerHand.cards.count == 2
|
||||||
|
let surrenderAvailable = settings.lateSurrender
|
||||||
|
let dealerHitsS17 = settings.dealerHitsSoft17
|
||||||
|
|
||||||
// Pairs
|
// 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) {
|
||||||
|
return String(localized: "Surrender")
|
||||||
|
}
|
||||||
|
// 15 vs 10 - Surrender
|
||||||
|
if playerValue == 15 && !isSoft && dealerValue == 10 {
|
||||||
|
return String(localized: "Surrender")
|
||||||
|
}
|
||||||
|
// 15 vs A - Surrender (if dealer hits soft 17)
|
||||||
|
if playerValue == 15 && !isSoft && dealerValue == 1 && dealerHitsS17 {
|
||||||
|
return String(localized: "Surrender")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PAIRS
|
||||||
if playerHand.canSplit {
|
if playerHand.canSplit {
|
||||||
let pairRank = playerHand.cards[0].rank
|
let pairRank = playerHand.cards[0].rank
|
||||||
switch pairRank {
|
switch pairRank {
|
||||||
case .ace, .eight:
|
case .ace:
|
||||||
|
return String(localized: "Split")
|
||||||
|
case .eight:
|
||||||
return String(localized: "Split")
|
return String(localized: "Split")
|
||||||
case .ten, .jack, .queen, .king:
|
case .ten, .jack, .queen, .king:
|
||||||
return String(localized: "Stand")
|
return String(localized: "Stand")
|
||||||
case .five:
|
case .five:
|
||||||
return canDouble ? String(localized: "Double") : String(localized: "Hit")
|
// Never split 5s - treat as hard 10
|
||||||
|
return (canDouble && dealerValue <= 9) ? String(localized: "Double") : String(localized: "Hit")
|
||||||
case .four:
|
case .four:
|
||||||
return (dealerValue == 5 || dealerValue == 6) ? String(localized: "Split") : String(localized: "Hit")
|
// Split 4s vs 5-6 (if DAS), otherwise hit
|
||||||
case .two, .three, .seven:
|
return (settings.doubleAfterSplit && (dealerValue == 5 || dealerValue == 6))
|
||||||
return dealerValue <= 7 ? String(localized: "Split") : String(localized: "Hit")
|
? 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:
|
case .six:
|
||||||
return dealerValue <= 6 ? String(localized: "Split") : String(localized: "Hit")
|
// 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:
|
case .nine:
|
||||||
return (dealerValue == 7 || dealerValue >= 10) ? String(localized: "Stand") : String(localized: "Split")
|
// 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
|
// SOFT HANDS (Ace counted as 11)
|
||||||
if isSoft {
|
if isSoft {
|
||||||
if playerValue >= 19 {
|
switch playerValue {
|
||||||
|
case 20, 21: // A,9 or A,10
|
||||||
return String(localized: "Stand")
|
return String(localized: "Stand")
|
||||||
}
|
case 19: // A,8
|
||||||
if playerValue == 18 {
|
// Double vs 6 if dealer hits S17, otherwise stand
|
||||||
if dealerValue >= 9 {
|
if canDouble && dealerValue == 6 && dealerHitsS17 {
|
||||||
return String(localized: "Hit")
|
return String(localized: "Double")
|
||||||
}
|
}
|
||||||
return String(localized: "Stand")
|
return String(localized: "Stand")
|
||||||
|
case 18: // A,7
|
||||||
|
// Double vs 3-6, Stand vs 2/7/8, Hit vs 9/10/A
|
||||||
|
if canDouble && dealerValue >= 3 && dealerValue <= 6 {
|
||||||
|
return String(localized: "Double")
|
||||||
|
}
|
||||||
|
if dealerValue == 2 || dealerValue == 7 || dealerValue == 8 {
|
||||||
|
return String(localized: "Stand")
|
||||||
}
|
}
|
||||||
// Soft 17 or less
|
|
||||||
return String(localized: "Hit")
|
return String(localized: "Hit")
|
||||||
|
case 17: // A,6
|
||||||
|
// Double vs 3-6, otherwise hit
|
||||||
|
if canDouble && dealerValue >= 3 && dealerValue <= 6 {
|
||||||
|
return String(localized: "Double")
|
||||||
|
}
|
||||||
|
return String(localized: "Hit")
|
||||||
|
case 16, 15: // A,5 or A,4
|
||||||
|
// Double vs 4-6, otherwise hit
|
||||||
|
if canDouble && dealerValue >= 4 && dealerValue <= 6 {
|
||||||
|
return String(localized: "Double")
|
||||||
|
}
|
||||||
|
return String(localized: "Hit")
|
||||||
|
case 14, 13: // A,3 or A,2
|
||||||
|
// Double vs 5-6, otherwise hit
|
||||||
|
if canDouble && dealerValue >= 5 && dealerValue <= 6 {
|
||||||
|
return String(localized: "Double")
|
||||||
|
}
|
||||||
|
return String(localized: "Hit")
|
||||||
|
default:
|
||||||
|
return String(localized: "Hit")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard hands
|
// HARD HANDS
|
||||||
if playerValue >= 17 {
|
switch playerValue {
|
||||||
|
case 17...21:
|
||||||
return String(localized: "Stand")
|
return String(localized: "Stand")
|
||||||
}
|
case 16:
|
||||||
if playerValue >= 13 && dealerValue <= 6 {
|
// Stand vs 2-6, Hit vs 7+
|
||||||
return String(localized: "Stand")
|
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
|
||||||
}
|
case 15:
|
||||||
if playerValue == 12 && dealerValue >= 4 && dealerValue <= 6 {
|
// Stand vs 2-6, Hit vs 7+
|
||||||
return String(localized: "Stand")
|
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
|
||||||
}
|
case 14:
|
||||||
if playerValue == 11 && canDouble {
|
// Stand vs 2-6, Hit vs 7+
|
||||||
|
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
|
||||||
|
case 13:
|
||||||
|
// Stand vs 2-6, Hit vs 7+
|
||||||
|
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
|
||||||
|
case 12:
|
||||||
|
// Stand vs 4-6, Hit vs 2-3 and 7+
|
||||||
|
return dealerValue >= 4 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
|
||||||
|
case 11:
|
||||||
|
// Always double (except vs A in some rules)
|
||||||
|
if canDouble {
|
||||||
return String(localized: "Double")
|
return String(localized: "Double")
|
||||||
}
|
}
|
||||||
if playerValue == 10 && dealerValue <= 9 && canDouble {
|
return String(localized: "Hit")
|
||||||
|
case 10:
|
||||||
|
// Double vs 2-9, Hit vs 10/A
|
||||||
|
if canDouble && dealerValue >= 2 && dealerValue <= 9 {
|
||||||
return String(localized: "Double")
|
return String(localized: "Double")
|
||||||
}
|
}
|
||||||
if playerValue == 9 && dealerValue >= 3 && dealerValue <= 6 && canDouble {
|
return String(localized: "Hit")
|
||||||
|
case 9:
|
||||||
|
// Double vs 3-6, Hit otherwise
|
||||||
|
if canDouble && dealerValue >= 3 && dealerValue <= 6 {
|
||||||
return String(localized: "Double")
|
return String(localized: "Double")
|
||||||
}
|
}
|
||||||
|
return String(localized: "Hit")
|
||||||
|
default: // 8 or less
|
||||||
|
return String(localized: "Hit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return String(localized: "Hit")
|
/// Returns the count-adjusted strategy recommendation with deviation explanation.
|
||||||
|
/// Based on the "Illustrious 18" - the most valuable count-based deviations.
|
||||||
|
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 isSoft = playerHand.isSoft
|
||||||
|
|
||||||
|
// Check for count-based deviations from basic strategy
|
||||||
|
|
||||||
|
// 16 vs 10: Stand at TC 0+ (basic says Hit)
|
||||||
|
if playerValue == 16 && !isSoft && dealerValue == 10 {
|
||||||
|
if tc >= 0 {
|
||||||
|
return String(localized: "Stand (Count: 16v10 at TC≥0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15 vs 10: Stand at TC +4+ (basic says Hit)
|
||||||
|
if playerValue == 15 && !isSoft && dealerValue == 10 {
|
||||||
|
if tc >= 4 {
|
||||||
|
return String(localized: "Stand (Count: 15v10 at TC≥+4)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12 vs 2: Stand at TC +3+ (basic says Hit)
|
||||||
|
if playerValue == 12 && !isSoft && dealerValue == 2 {
|
||||||
|
if tc >= 3 {
|
||||||
|
return String(localized: "Stand (Count: 12v2 at TC≥+3)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12 vs 3: Stand at TC +2+ (basic says Hit)
|
||||||
|
if playerValue == 12 && !isSoft && dealerValue == 3 {
|
||||||
|
if tc >= 2 {
|
||||||
|
return String(localized: "Stand (Count: 12v3 at TC≥+2)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12 vs 4: Hit at TC < 0 (basic says Stand)
|
||||||
|
if playerValue == 12 && !isSoft && dealerValue == 4 {
|
||||||
|
if tc < 0 {
|
||||||
|
return String(localized: "Hit (Count: 12v4 at TC<0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13 vs 2: Hit at TC < -1 (basic says Stand)
|
||||||
|
if playerValue == 13 && !isSoft && dealerValue == 2 {
|
||||||
|
if tc < -1 {
|
||||||
|
return String(localized: "Hit (Count: 13v2 at TC<-1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16 vs 9: Stand at TC +5+ (basic says Hit)
|
||||||
|
if playerValue == 16 && !isSoft && dealerValue == 9 {
|
||||||
|
if tc >= 5 {
|
||||||
|
return String(localized: "Stand (Count: 16v9 at TC≥+5)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10 vs 10: Double at TC +4+ (basic says Hit)
|
||||||
|
if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 10 {
|
||||||
|
if tc >= 4 {
|
||||||
|
return String(localized: "Double (Count: 10v10 at TC≥+4)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10 vs A: Double at TC +4+ (basic says Hit)
|
||||||
|
if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 1 {
|
||||||
|
if tc >= 4 {
|
||||||
|
return String(localized: "Double (Count: 10vA at TC≥+4)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9 vs 2: Double at TC +1+ (basic says Hit)
|
||||||
|
if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 2 {
|
||||||
|
if tc >= 1 {
|
||||||
|
return String(localized: "Double (Count: 9v2 at TC≥+1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9 vs 7: Double at TC +3+ (basic says Hit)
|
||||||
|
if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 7 {
|
||||||
|
if tc >= 3 {
|
||||||
|
return String(localized: "Double (Count: 9v7 at TC≥+3)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pair of 10s vs 5: Split at TC +5+ (basic says Stand)
|
||||||
|
if playerHand.canSplit && playerHand.cards[0].blackjackValue == 10 && dealerValue == 5 {
|
||||||
|
if tc >= 5 {
|
||||||
|
return String(localized: "Split (Count: 10,10v5 at TC≥+5)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pair of 10s vs 6: Split at TC +4+ (basic says Stand)
|
||||||
|
if playerHand.canSplit && playerHand.cards[0].blackjackValue == 10 && dealerValue == 6 {
|
||||||
|
if tc >= 4 {
|
||||||
|
return String(localized: "Split (Count: 10,10v6 at TC≥+4)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No deviation applies, return basic strategy
|
||||||
|
return basicHint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,9 @@ final class GameState {
|
|||||||
/// Insurance bet amount.
|
/// Insurance bet amount.
|
||||||
var insuranceBet: Int = 0
|
var insuranceBet: Int = 0
|
||||||
|
|
||||||
|
/// Whether a reshuffle notification should be shown.
|
||||||
|
var showReshuffleNotification: Bool = false
|
||||||
|
|
||||||
// MARK: - Hands
|
// MARK: - Hands
|
||||||
|
|
||||||
/// Player's hands (can have multiple after splits).
|
/// Player's hands (can have multiple after splits).
|
||||||
@ -265,6 +268,17 @@ final class GameState {
|
|||||||
func deal() async {
|
func deal() async {
|
||||||
guard canDeal else { return }
|
guard canDeal else { return }
|
||||||
|
|
||||||
|
// Ensure enough cards for a full hand - reshuffle if needed
|
||||||
|
if !engine.canDealNewHand {
|
||||||
|
engine.reshuffle()
|
||||||
|
showReshuffleNotification = true
|
||||||
|
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .seconds(2))
|
||||||
|
showReshuffleNotification = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentPhase = .dealing
|
currentPhase = .dealing
|
||||||
playerHands = [BlackjackHand(bet: currentBet)]
|
playerHands = [BlackjackHand(bet: currentBet)]
|
||||||
dealerHand = BlackjackHand()
|
dealerHand = BlackjackHand()
|
||||||
@ -645,6 +659,13 @@ final class GameState {
|
|||||||
// Check if shoe needs reshuffling
|
// Check if shoe needs reshuffling
|
||||||
if engine.needsReshuffle {
|
if engine.needsReshuffle {
|
||||||
engine.reshuffle()
|
engine.reshuffle()
|
||||||
|
showReshuffleNotification = true
|
||||||
|
|
||||||
|
// Auto-dismiss after a delay
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(for: .seconds(2))
|
||||||
|
showReshuffleNotification = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -143,6 +143,9 @@ final class GameSettings {
|
|||||||
/// Whether to show dealer hints (suggested action).
|
/// Whether to show dealer hints (suggested action).
|
||||||
var showHints: Bool = true { didSet { save() } }
|
var showHints: Bool = true { didSet { save() } }
|
||||||
|
|
||||||
|
/// Whether to show the running card count (Hi-Lo system).
|
||||||
|
var showCardCount: Bool = false { didSet { save() } }
|
||||||
|
|
||||||
// MARK: - Sound Settings
|
// MARK: - Sound Settings
|
||||||
|
|
||||||
/// Whether sound effects are enabled.
|
/// Whether sound effects are enabled.
|
||||||
@ -234,6 +237,7 @@ final class GameSettings {
|
|||||||
self.showCardsRemaining = data.showCardsRemaining
|
self.showCardsRemaining = data.showCardsRemaining
|
||||||
self.showHistory = data.showHistory
|
self.showHistory = data.showHistory
|
||||||
self.showHints = data.showHints
|
self.showHints = data.showHints
|
||||||
|
self.showCardCount = data.showCardCount
|
||||||
self.soundEnabled = data.soundEnabled
|
self.soundEnabled = data.soundEnabled
|
||||||
self.hapticsEnabled = data.hapticsEnabled
|
self.hapticsEnabled = data.hapticsEnabled
|
||||||
self.soundVolume = data.soundVolume
|
self.soundVolume = data.soundVolume
|
||||||
@ -258,6 +262,7 @@ final class GameSettings {
|
|||||||
showCardsRemaining: showCardsRemaining,
|
showCardsRemaining: showCardsRemaining,
|
||||||
showHistory: showHistory,
|
showHistory: showHistory,
|
||||||
showHints: showHints,
|
showHints: showHints,
|
||||||
|
showCardCount: showCardCount,
|
||||||
soundEnabled: soundEnabled,
|
soundEnabled: soundEnabled,
|
||||||
hapticsEnabled: hapticsEnabled,
|
hapticsEnabled: hapticsEnabled,
|
||||||
soundVolume: soundVolume
|
soundVolume: soundVolume
|
||||||
|
|||||||
@ -131,5 +131,29 @@ extension Card {
|
|||||||
case .ten, .jack, .queen, .king: return 10
|
case .ten, .jack, .queen, .king: return 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The Hi-Lo card counting value.
|
||||||
|
/// Low cards (2-6): +1 (good for player when removed)
|
||||||
|
/// Neutral (7-9): 0
|
||||||
|
/// High cards (10-A): -1 (bad for player when removed)
|
||||||
|
var hiLoValue: Int {
|
||||||
|
switch rank {
|
||||||
|
case .two, .three, .four, .five, .six:
|
||||||
|
return 1 // Low cards
|
||||||
|
case .seven, .eight, .nine:
|
||||||
|
return 0 // Neutral
|
||||||
|
case .ten, .jack, .queen, .king, .ace:
|
||||||
|
return -1 // High cards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display text for the Hi-Lo count value.
|
||||||
|
var hiLoDisplayText: String {
|
||||||
|
switch hiLoValue {
|
||||||
|
case 1: return "+1"
|
||||||
|
case -1: return "-1"
|
||||||
|
default: return "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
"%@" : {
|
||||||
|
"comment" : "A label displaying the current true count for a card counting practice session. The argument is the true count, formatted to one decimal place.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"%lld" : {
|
"%lld" : {
|
||||||
"comment" : "A label displaying the amount wagered in the current hand. The argument is the amount wagered in the current hand.",
|
"comment" : "A label displaying the amount wagered in the current hand. The argument is the amount wagered in the current hand.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -39,11 +43,41 @@
|
|||||||
"comment" : "A step in the process of exporting app icons.",
|
"comment" : "A step in the process of exporting app icons.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"+%@" : {
|
||||||
|
"comment" : "A label showing the true count for card counting. The number is formatted to one decimal place.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"+%lld" : {
|
||||||
|
"comment" : "A label that displays the running count of cards in a Hi-Lo card counting practice session. The text inside the label changes color based on whether the count is positive or negative.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"1 Deck: Lowest house edge (~0.17%), rare to find." : {
|
"1 Deck: Lowest house edge (~0.17%), rare to find." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"2 Decks: Low house edge (~0.35%), common online." : {
|
"2 Decks: Low house edge (~0.35%), common online." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"2-6: +1 (low cards favor house)" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "2-6: +1 (low cards favor house)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "2-6: +1 (cartas bajas favorecen la casa)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "2-6: +1 (les cartes basses favorisent la maison)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"2-10: Face value" : {
|
"2-10: Face value" : {
|
||||||
"comment" : "Description of the card values for cards with values 2 through 10.",
|
"comment" : "Description of the card values for cards with values 2 through 10.",
|
||||||
@ -60,12 +94,56 @@
|
|||||||
},
|
},
|
||||||
"6:5 Blackjack (avoid!): Increases house edge by ~1.4%." : {
|
"6:5 Blackjack (avoid!): Increases house edge by ~1.4%." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"7-9: 0 (neutral cards)" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "7-9: 0 (neutral cards)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "7-9: 0 (cartas neutrales)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "7-9: 0 (cartes neutres)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"8 decks shuffled together." : {
|
"8 decks shuffled together." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"8 Decks: Standard in Atlantic City (~0.55%)." : {
|
"8 Decks: Standard in Atlantic City (~0.55%)." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"10-A: -1 (high cards favor player)" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "10-A: -1 (high cards favor player)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "10-A: -1 (cartas altas favorecen al jugador)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "10-A: -1 (les cartes hautes favorisent le joueur)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"A 'soft' hand has an Ace counting as 11." : {
|
"A 'soft' hand has an Ace counting as 11." : {
|
||||||
"comment" : "Explanation of how an Ace can be counted as either 1 or 11 in a hand.",
|
"comment" : "Explanation of how an Ace can be counted as either 1 or 11 in a hand.",
|
||||||
@ -217,7 +295,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Basic Strategy" : {
|
"Basic Strategy" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Basic Strategy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Estrategia Básica"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Stratégie de Base"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Basic strategy suggestions" : {
|
"Basic strategy suggestions" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -267,6 +364,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Bet 2x minimum" : {
|
||||||
|
"comment" : "Betting recommendation based on a true count of 1.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Bet 4x minimum" : {
|
||||||
|
"comment" : "Betting recommendation based on the true count of 4.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Bet 6x minimum" : {
|
||||||
|
"comment" : "Betting recommendation based on a true count of 3.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Bet 8x minimum" : {
|
||||||
|
"comment" : "Betting recommendation based on the true count of 4.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Bet maximum!" : {
|
||||||
|
"comment" : "Betting recommendation to bet the maximum amount when the true count is 5 or higher.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Bet minimum" : {
|
||||||
|
"comment" : "Betting recommendation to bet the minimum amount.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Bet minimum (neutral)" : {
|
||||||
|
"comment" : "Betting hint when the true count is 0.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Bet minimum or sit out" : {
|
||||||
|
"comment" : "Betting recommendation to bet the minimum amount or to sit out when the true count is -2 or lower.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Betting Hint" : {
|
||||||
|
"comment" : "A label describing the view that shows betting recommendations.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"BIGGEST SWINGS" : {
|
"BIGGEST SWINGS" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -454,6 +587,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Card Count" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Card Count"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Conteo de cartas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Compte des cartes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Card Counting" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Card Counting"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Conteo de Cartas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Comptage des Cartes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Card dealing animations" : {
|
"Card dealing animations" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -571,6 +748,13 @@
|
|||||||
"Costs half your original bet." : {
|
"Costs half your original bet." : {
|
||||||
"comment" : "Description of the cost of insurance in the rules help view.",
|
"comment" : "Description of the cost of insurance in the rules help view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Count reset to 0" : {
|
||||||
|
"comment" : "A description of what happens when the shoe is reshuffled in Blackjack.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Count resets to 0 when the shoe is shuffled." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Custom" : {
|
"Custom" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -791,6 +975,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Decrease bets when the count is negative." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"DISPLAY" : {
|
"DISPLAY" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -858,6 +1045,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Double (Count: 9v2 at TC≥+1)" : {
|
||||||
|
"comment" : "Explanation of a count-based deviation from the basic strategy, recommending to double down.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Double (Count: 9v7 at TC≥+3)" : {
|
||||||
|
"comment" : "Strategy recommendation to double down when the player has a value of 9, is soft, has two cards, and the dealer's upcard is 7, with a true count of 3 or higher.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Double (Count: 10v10 at TC≥+4)" : {
|
||||||
|
"comment" : "Explanation of a recommended Blackjack move based on a count-adjusted strategy, specifically for a pair of 10s against a 10.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Double (Count: 10vA at TC≥+4)" : {
|
||||||
|
"comment" : "Text displayed in a notification when a user should double their bet in a Blackjack game.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Double After Split" : {
|
"Double After Split" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -933,6 +1136,9 @@
|
|||||||
"Double tap to add chips" : {
|
"Double tap to add chips" : {
|
||||||
"comment" : "A hint that appears when a user taps on the betting zone, instructing them to double-tap to add chips.",
|
"comment" : "A hint that appears when a user taps on the betting zone, instructing them to double-tap to add chips.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Enable 'Card Count' in Settings to practice." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"European" : {
|
"European" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -955,6 +1161,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Fewer decks = easier to count accurately." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Fewer decks favor the player slightly." : {
|
"Fewer decks favor the player slightly." : {
|
||||||
|
|
||||||
@ -1063,8 +1272,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Hi-Lo is the most popular counting system." : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Hi-Lo is the most popular counting system."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Hi-Lo es el sistema de conteo más popular."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Hi-Lo est le système de comptage le plus populaire."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Higher house edge due to no hole card." : {
|
"Higher house edge due to no hole card." : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Higher house edge due to no hole card."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mayor ventaja de la casa debido a la ausencia de carta oculta."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Avantage de la maison plus élevé en raison de l'absence de carte cachée."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Hint: %@" : {
|
"Hint: %@" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1110,6 +1360,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Hit (Count: 12v4 at TC<0)" : {
|
||||||
|
"comment" : "Explanation of a Blackjack hand value and true count that leads to recommending to hit.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Hit (Count: 13v2 at TC<-1)" : {
|
||||||
|
"comment" : "Explanation of a Blackjack hand value and the recommended action based on the true count.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Hit on soft 17 or less." : {
|
"Hit on soft 17 or less." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -1162,6 +1420,9 @@
|
|||||||
"If you go over 21, you 'bust' and lose immediately." : {
|
"If you go over 21, you 'bust' and lose immediately." : {
|
||||||
"comment" : "Description of the outcome when a player's hand value exceeds 21.",
|
"comment" : "Description of the outcome when a player's hand value exceeds 21.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Increase bets when the count is positive." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Insurance" : {
|
"Insurance" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1664,6 +1925,9 @@
|
|||||||
},
|
},
|
||||||
"Poker" : {
|
"Poker" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Positive count = more high cards remain = player advantage." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"PUSH" : {
|
"PUSH" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1795,6 +2059,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Running" : {
|
||||||
|
"comment" : "The text \"Running\" displayed in the card count view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Running %lld, True %@" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Running %lld, True %@"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Actual %lld, Verdadero %@"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Courant %lld, Vrai %@"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Running Count: Sum of all card values seen." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"SESSION SUMMARY" : {
|
"SESSION SUMMARY" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1840,6 +2133,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Shoe reshuffled" : {
|
||||||
|
"comment" : "A description of the action of reshuffling a shoe.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Shoe Reshuffled" : {
|
||||||
|
"comment" : "A title for the reshuffle notification.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Shoe reshuffled, count reset to zero" : {
|
||||||
|
"comment" : "A description of the accessibility label for the reshuffle notification view when the card count is reset.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Show Animations" : {
|
"Show Animations" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1884,6 +2189,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Show Hi-Lo running count & card values" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Show Hi-Lo running count & card values"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Mostrar conteo Hi-Lo y valores de cartas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Afficher le compte Hi-Lo et les valeurs des cartes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Show Hints" : {
|
"Show Hints" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -1980,6 +2307,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Split (Count: 10,10v5 at TC≥+5)" : {
|
||||||
|
"comment" : "Description of a strategy recommendation to split a pair of 10s when the true count is greater than or equal to 5.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Split (Count: 10,10v6 at TC≥+4)" : {
|
||||||
|
"comment" : "Description of a count-based deviation from the basic strategy, specifically for a pair of 10s against a 6.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Split Hand" : {
|
"Split Hand" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -2031,6 +2366,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Stand (Count: 12v2 at TC≥+3)" : {
|
||||||
|
"comment" : "Explanation of a count-based deviation from the basic strategy, including the count and the recommended action.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Stand (Count: 12v3 at TC≥+2)" : {
|
||||||
|
"comment" : "Explanation of a count-adjusted strategy recommendation to stand in a 12-3 situation with a true count of 2.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Stand (Count: 15v10 at TC≥+4)" : {
|
||||||
|
"comment" : "Explanation of a count-based deviation from the basic strategy.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Stand (Count: 16v9 at TC≥+5)" : {
|
||||||
|
"comment" : "Explanation of a Blackjack game hint based on a count-adjusted strategy recommendation.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Stand (Count: 16v10 at TC≥0)" : {
|
||||||
|
"comment" : "Explanation of a count-based deviation from the basic strategy, indicating that the recommended action is to stand.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Stand on 17+ always." : {
|
"Stand on 17+ always." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -2216,10 +2571,61 @@
|
|||||||
},
|
},
|
||||||
"Traditional European casino style." : {
|
"Traditional European casino style." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"True" : {
|
||||||
|
"comment" : "A label displayed above the true count of a card counting practice session.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"True count of +2 or higher favors the player." : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "True count of +2 or higher favors the player."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Cuenta verdadera de +2 o más favorece al jugador."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Un compte vrai de +2 ou plus favorise le joueur."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"True Count: Running count ÷ decks remaining." : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "True Count: Running count ÷ decks remaining."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-MX" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Cuenta Verdadera: Cuenta actual ÷ mazos restantes."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Compte Vrai: Compte courant ÷ paquets restants."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Upload the 1024px icon to appicon.co or makeappicon.com to generate all sizes automatically." : {
|
"Upload the 1024px icon to appicon.co or makeappicon.com to generate all sizes automatically." : {
|
||||||
"comment" : "A description of an alternative method for generating app icons.",
|
"comment" : "A description of an alternative method for generating app icons.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Using the Count" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Vegas Strip" : {
|
"Vegas Strip" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@ -71,6 +71,7 @@ struct BlackjackSettingsData: PersistableGameData {
|
|||||||
showCardsRemaining: true,
|
showCardsRemaining: true,
|
||||||
showHistory: true,
|
showHistory: true,
|
||||||
showHints: true,
|
showHints: true,
|
||||||
|
showCardCount: false,
|
||||||
soundEnabled: true,
|
soundEnabled: true,
|
||||||
hapticsEnabled: true,
|
hapticsEnabled: true,
|
||||||
soundVolume: 1.0
|
soundVolume: 1.0
|
||||||
@ -93,6 +94,7 @@ struct BlackjackSettingsData: PersistableGameData {
|
|||||||
var showCardsRemaining: Bool
|
var showCardsRemaining: Bool
|
||||||
var showHistory: Bool
|
var showHistory: Bool
|
||||||
var showHints: Bool
|
var showHints: Bool
|
||||||
|
var showCardCount: Bool
|
||||||
var soundEnabled: Bool
|
var soundEnabled: Bool
|
||||||
var hapticsEnabled: Bool
|
var hapticsEnabled: Bool
|
||||||
var soundVolume: Float
|
var soundVolume: Float
|
||||||
|
|||||||
@ -12,6 +12,9 @@ struct BlackjackTableView: View {
|
|||||||
@Bindable var state: GameState
|
@Bindable var state: GameState
|
||||||
let onPlaceBet: () -> Void
|
let onPlaceBet: () -> Void
|
||||||
|
|
||||||
|
/// Whether to show Hi-Lo card count values on cards.
|
||||||
|
var showCardCount: Bool { state.settings.showCardCount }
|
||||||
|
|
||||||
// MARK: - Scaled Metrics
|
// MARK: - Scaled Metrics
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
|
||||||
@ -29,6 +32,7 @@ struct BlackjackTableView: View {
|
|||||||
DealerHandView(
|
DealerHandView(
|
||||||
hand: state.dealerHand,
|
hand: state.dealerHand,
|
||||||
showHoleCard: shouldShowDealerHoleCard,
|
showHoleCard: shouldShowDealerHoleCard,
|
||||||
|
showCardCount: showCardCount,
|
||||||
cardWidth: cardWidth,
|
cardWidth: cardWidth,
|
||||||
cardSpacing: cardSpacing
|
cardSpacing: cardSpacing
|
||||||
)
|
)
|
||||||
@ -51,6 +55,7 @@ struct BlackjackTableView: View {
|
|||||||
hands: state.playerHands,
|
hands: state.playerHands,
|
||||||
activeHandIndex: state.activeHandIndex,
|
activeHandIndex: state.activeHandIndex,
|
||||||
isPlayerTurn: isPlayerTurn,
|
isPlayerTurn: isPlayerTurn,
|
||||||
|
showCardCount: showCardCount,
|
||||||
cardWidth: cardWidth,
|
cardWidth: cardWidth,
|
||||||
cardSpacing: cardSpacing
|
cardSpacing: cardSpacing
|
||||||
)
|
)
|
||||||
@ -64,6 +69,12 @@ struct BlackjackTableView: View {
|
|||||||
onTap: onPlaceBet
|
onTap: onPlaceBet
|
||||||
)
|
)
|
||||||
.transition(.scale.combined(with: .opacity))
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
|
||||||
|
// Betting hint based on count (only when card counting enabled)
|
||||||
|
if showCardCount, let bettingHint = bettingHint {
|
||||||
|
BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hint (when enabled and player turn)
|
// Hint (when enabled and player turn)
|
||||||
@ -98,8 +109,40 @@ struct BlackjackTableView: View {
|
|||||||
private var currentHint: String? {
|
private var currentHint: String? {
|
||||||
guard let hand = state.activeHand,
|
guard let hand = state.activeHand,
|
||||||
let upCard = state.dealerUpCard else { return nil }
|
let upCard = state.dealerUpCard else { return nil }
|
||||||
|
|
||||||
|
// Use count-adjusted hints when card counting is enabled
|
||||||
|
if showCardCount {
|
||||||
|
return state.engine.getCountAdjustedHint(playerHand: hand, dealerUpCard: upCard)
|
||||||
|
}
|
||||||
return state.engine.getHint(playerHand: hand, dealerUpCard: upCard)
|
return state.engine.getHint(playerHand: hand, dealerUpCard: upCard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Betting recommendation based on the true count.
|
||||||
|
private var bettingHint: String? {
|
||||||
|
let tc = Int(state.engine.trueCount.rounded())
|
||||||
|
|
||||||
|
// Betting spread recommendations based on true count
|
||||||
|
switch tc {
|
||||||
|
case ...(-2):
|
||||||
|
return String(localized: "Bet minimum or sit out")
|
||||||
|
case -1:
|
||||||
|
return String(localized: "Bet minimum")
|
||||||
|
case 0:
|
||||||
|
return String(localized: "Bet minimum (neutral)")
|
||||||
|
case 1:
|
||||||
|
return String(localized: "Bet 2x minimum")
|
||||||
|
case 2:
|
||||||
|
return String(localized: "Bet 4x minimum")
|
||||||
|
case 3:
|
||||||
|
return String(localized: "Bet 6x minimum")
|
||||||
|
case 4:
|
||||||
|
return String(localized: "Bet 8x minimum")
|
||||||
|
case 5...:
|
||||||
|
return String(localized: "Bet maximum!")
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Dealer Hand View
|
// MARK: - Dealer Hand View
|
||||||
@ -107,6 +150,7 @@ struct BlackjackTableView: View {
|
|||||||
struct DealerHandView: View {
|
struct DealerHandView: View {
|
||||||
let hand: BlackjackHand
|
let hand: BlackjackHand
|
||||||
let showHoleCard: Bool
|
let showHoleCard: Bool
|
||||||
|
let showCardCount: Bool
|
||||||
let cardWidth: CGFloat
|
let cardWidth: CGFloat
|
||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
|
|
||||||
@ -144,6 +188,11 @@ struct DealerHandView: View {
|
|||||||
isFaceUp: isFaceUp,
|
isFaceUp: isFaceUp,
|
||||||
cardWidth: cardWidth
|
cardWidth: cardWidth
|
||||||
)
|
)
|
||||||
|
.overlay(alignment: .bottomLeading) {
|
||||||
|
if showCardCount && isFaceUp {
|
||||||
|
HiLoCountBadge(card: hand.cards[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
.zIndex(Double(index))
|
.zIndex(Double(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +253,7 @@ struct PlayerHandsView: View {
|
|||||||
let hands: [BlackjackHand]
|
let hands: [BlackjackHand]
|
||||||
let activeHandIndex: Int
|
let activeHandIndex: Int
|
||||||
let isPlayerTurn: Bool
|
let isPlayerTurn: Bool
|
||||||
|
let showCardCount: Bool
|
||||||
let cardWidth: CGFloat
|
let cardWidth: CGFloat
|
||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
|
|
||||||
@ -239,6 +289,7 @@ struct PlayerHandsView: View {
|
|||||||
PlayerHandView(
|
PlayerHandView(
|
||||||
hand: hands[index],
|
hand: hands[index],
|
||||||
isActive: index == activeHandIndex && isPlayerTurn,
|
isActive: index == activeHandIndex && isPlayerTurn,
|
||||||
|
showCardCount: showCardCount,
|
||||||
// Hand numbers: rightmost is Hand 1, leftmost is Hand 2, etc.
|
// Hand numbers: rightmost is Hand 1, leftmost is Hand 2, etc.
|
||||||
handNumber: hands.count > 1 ? hands.count - index : nil,
|
handNumber: hands.count > 1 ? hands.count - index : nil,
|
||||||
cardWidth: adaptiveCardWidth,
|
cardWidth: adaptiveCardWidth,
|
||||||
@ -253,6 +304,7 @@ struct PlayerHandsView: View {
|
|||||||
struct PlayerHandView: View {
|
struct PlayerHandView: View {
|
||||||
let hand: BlackjackHand
|
let hand: BlackjackHand
|
||||||
let isActive: Bool
|
let isActive: Bool
|
||||||
|
let showCardCount: Bool
|
||||||
let handNumber: Int?
|
let handNumber: Int?
|
||||||
let cardWidth: CGFloat
|
let cardWidth: CGFloat
|
||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
@ -274,6 +326,11 @@ struct PlayerHandView: View {
|
|||||||
isFaceUp: true,
|
isFaceUp: true,
|
||||||
cardWidth: cardWidth
|
cardWidth: cardWidth
|
||||||
)
|
)
|
||||||
|
.overlay(alignment: .bottomLeading) {
|
||||||
|
if showCardCount {
|
||||||
|
HiLoCountBadge(card: hand.cards[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
.zIndex(Double(index))
|
.zIndex(Double(index))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -478,6 +535,59 @@ struct InsuranceZoneView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Betting Hint View
|
||||||
|
|
||||||
|
/// Shows betting recommendations based on the current count.
|
||||||
|
struct BettingHintView: View {
|
||||||
|
let hint: String
|
||||||
|
let trueCount: Double
|
||||||
|
|
||||||
|
private var hintColor: Color {
|
||||||
|
let tc = Int(trueCount.rounded())
|
||||||
|
if tc >= 2 {
|
||||||
|
return .green // Player advantage - bet more
|
||||||
|
} else if tc <= -1 {
|
||||||
|
return .red // House advantage - bet less
|
||||||
|
} else {
|
||||||
|
return .yellow // Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var icon: String {
|
||||||
|
let tc = Int(trueCount.rounded())
|
||||||
|
if tc >= 2 {
|
||||||
|
return "arrow.up.circle.fill" // Increase bet
|
||||||
|
} else if tc <= -1 {
|
||||||
|
return "arrow.down.circle.fill" // Decrease bet
|
||||||
|
} else {
|
||||||
|
return "equal.circle.fill" // Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: Design.Spacing.small) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.foregroundStyle(hintColor)
|
||||||
|
Text(hint)
|
||||||
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.black.opacity(Design.Opacity.light))
|
||||||
|
.overlay(
|
||||||
|
Capsule()
|
||||||
|
.strokeBorder(hintColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(String(localized: "Betting Hint"))
|
||||||
|
.accessibilityValue(hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Hint View
|
// MARK: - Hint View
|
||||||
|
|
||||||
struct HintView: View {
|
struct HintView: View {
|
||||||
@ -500,6 +610,38 @@ struct HintView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Hi-Lo Count Badge
|
||||||
|
|
||||||
|
/// A small badge showing the Hi-Lo counting value of a card.
|
||||||
|
struct HiLoCountBadge: View {
|
||||||
|
let card: Card
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(card.hiLoDisplayText)
|
||||||
|
.font(.system(size: Design.BaseFontSize.xxSmall, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(badgeTextColor)
|
||||||
|
.padding(.horizontal, Design.Spacing.xSmall)
|
||||||
|
.padding(.vertical, Design.Spacing.xxxSmall)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(badgeBackgroundColor)
|
||||||
|
)
|
||||||
|
.offset(x: -Design.Spacing.xSmall, y: Design.Spacing.xSmall)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var badgeBackgroundColor: Color {
|
||||||
|
switch card.hiLoValue {
|
||||||
|
case 1: return .green // Low cards = positive for player
|
||||||
|
case -1: return .red // High cards = negative for player
|
||||||
|
default: return .gray // Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var badgeTextColor: Color {
|
||||||
|
.white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Card Accessibility Extension
|
// MARK: - Card Accessibility Extension
|
||||||
|
|
||||||
extension Card {
|
extension Card {
|
||||||
|
|||||||
@ -86,6 +86,22 @@ struct GameTableView: View {
|
|||||||
)
|
)
|
||||||
.frame(maxWidth: maxContentWidth)
|
.frame(maxWidth: maxContentWidth)
|
||||||
|
|
||||||
|
// Card count display (when enabled)
|
||||||
|
if settings.showCardCount {
|
||||||
|
CardCountView(
|
||||||
|
runningCount: state.engine.runningCount,
|
||||||
|
trueCount: state.engine.trueCount
|
||||||
|
)
|
||||||
|
.frame(maxWidth: maxContentWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reshuffle notification
|
||||||
|
if state.showReshuffleNotification {
|
||||||
|
ReshuffleNotificationView(showCardCount: settings.showCardCount)
|
||||||
|
.frame(maxWidth: maxContentWidth)
|
||||||
|
.transition(.move(edge: .top).combined(with: .opacity))
|
||||||
|
}
|
||||||
|
|
||||||
// Table layout
|
// Table layout
|
||||||
BlackjackTableView(
|
BlackjackTableView(
|
||||||
state: state,
|
state: state,
|
||||||
@ -337,6 +353,99 @@ struct ActionButton: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Card Count View
|
||||||
|
|
||||||
|
/// Displays the Hi-Lo running count for card counting practice.
|
||||||
|
struct CardCountView: View {
|
||||||
|
let runningCount: Int
|
||||||
|
let trueCount: Double
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: Design.Spacing.large) {
|
||||||
|
// Running count
|
||||||
|
VStack(spacing: Design.Spacing.xxSmall) {
|
||||||
|
Text("Running")
|
||||||
|
.font(.system(size: Design.BaseFontSize.xSmall, weight: .medium))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
|
Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)")
|
||||||
|
.font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundStyle(countColor(for: runningCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: Design.Spacing.xLarge)
|
||||||
|
.background(Color.white.opacity(Design.Opacity.hint))
|
||||||
|
|
||||||
|
// True count
|
||||||
|
VStack(spacing: Design.Spacing.xxSmall) {
|
||||||
|
Text("True")
|
||||||
|
.font(.system(size: Design.BaseFontSize.xSmall, weight: .medium))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
|
Text(trueCount >= 0 ? "+\(trueCount, format: .number.precision(.fractionLength(1)))" : "\(trueCount, format: .number.precision(.fractionLength(1)))")
|
||||||
|
.font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundStyle(countColor(for: Int(trueCount.rounded())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
||||||
|
.fill(Color.black.opacity(Design.Opacity.subtle))
|
||||||
|
)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(String(localized: "Card Count"))
|
||||||
|
.accessibilityValue(String(localized: "Running \(runningCount), True \(trueCount, format: .number.precision(.fractionLength(1)))"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func countColor(for count: Int) -> Color {
|
||||||
|
if count > 0 {
|
||||||
|
return .green // Positive count favors player
|
||||||
|
} else if count < 0 {
|
||||||
|
return .red // Negative count favors house
|
||||||
|
} else {
|
||||||
|
return .white // Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Reshuffle Notification View
|
||||||
|
|
||||||
|
/// Shows a notification when the shoe is reshuffled.
|
||||||
|
struct ReshuffleNotificationView: View {
|
||||||
|
let showCardCount: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: Design.Spacing.small) {
|
||||||
|
Image(systemName: "shuffle")
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
|
Text("Shoe Reshuffled")
|
||||||
|
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
if showCardCount {
|
||||||
|
Text("Count reset to 0")
|
||||||
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
|
.padding(.vertical, Design.Spacing.medium)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.blue.opacity(Design.Opacity.heavy))
|
||||||
|
)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(showCardCount
|
||||||
|
? String(localized: "Shoe reshuffled, count reset to zero")
|
||||||
|
: String(localized: "Shoe reshuffled"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@ -160,6 +160,31 @@ struct RulesHelpView: View {
|
|||||||
String(localized: "Hit on soft 17 or less."),
|
String(localized: "Hit on soft 17 or less."),
|
||||||
String(localized: "Surrender 16 vs dealer 9, 10, Ace.")
|
String(localized: "Surrender 16 vs dealer 9, 10, Ace.")
|
||||||
]
|
]
|
||||||
|
),
|
||||||
|
RulePage(
|
||||||
|
title: String(localized: "Card Counting"),
|
||||||
|
icon: "number.circle.fill",
|
||||||
|
content: [
|
||||||
|
String(localized: "Hi-Lo is the most popular counting system."),
|
||||||
|
String(localized: "2-6: +1 (low cards favor house)"),
|
||||||
|
String(localized: "7-9: 0 (neutral cards)"),
|
||||||
|
String(localized: "10-A: -1 (high cards favor player)"),
|
||||||
|
String(localized: "Running Count: Sum of all card values seen."),
|
||||||
|
String(localized: "True Count: Running count ÷ decks remaining."),
|
||||||
|
String(localized: "Positive count = more high cards remain = player advantage.")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
RulePage(
|
||||||
|
title: String(localized: "Using the Count"),
|
||||||
|
icon: "chart.line.uptrend.xyaxis",
|
||||||
|
content: [
|
||||||
|
String(localized: "True count of +2 or higher favors the player."),
|
||||||
|
String(localized: "Increase bets when the count is positive."),
|
||||||
|
String(localized: "Decrease bets when the count is negative."),
|
||||||
|
String(localized: "Fewer decks = easier to count accurately."),
|
||||||
|
String(localized: "Count resets to 0 when the shoe is shuffled."),
|
||||||
|
String(localized: "Enable 'Card Count' in Settings to practice.")
|
||||||
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,14 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
||||||
|
|
||||||
|
SettingsToggle(
|
||||||
|
title: String(localized: "Card Count"),
|
||||||
|
subtitle: String(localized: "Show Hi-Lo running count & card values"),
|
||||||
|
isOn: $settings.showCardCount
|
||||||
|
)
|
||||||
|
|
||||||
|
Divider().background(Color.white.opacity(Design.Opacity.hint))
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: String(localized: "Cards Remaining"),
|
title: String(localized: "Cards Remaining"),
|
||||||
subtitle: String(localized: "Show cards left in shoe"),
|
subtitle: String(localized: "Show cards left in shoe"),
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public enum CasinoDesign {
|
|||||||
// MARK: - Spacing
|
// MARK: - Spacing
|
||||||
|
|
||||||
public enum Spacing {
|
public enum Spacing {
|
||||||
|
public static let xxxSmall: CGFloat = 1
|
||||||
public static let xxSmall: CGFloat = 2
|
public static let xxSmall: CGFloat = 2
|
||||||
public static let xSmall: CGFloat = 4
|
public static let xSmall: CGFloat = 4
|
||||||
public static let small: CGFloat = 8
|
public static let small: CGFloat = 8
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user