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

This commit is contained in:
Matt Bruce 2025-12-25 09:07:28 -06:00
parent 889e91a8ca
commit 98d72d0db8
11 changed files with 202 additions and 41 deletions

View File

@ -485,7 +485,11 @@ final class GameState {
evaluateSideBets() evaluateSideBets()
// Check for insurance offer (only in American style with hole card) // Check for insurance offer (only in American style with hole card)
if !settings.noHoleCard, let upCard = dealerUpCard, engine.shouldOfferInsurance(dealerUpCard: upCard) { // Skip if user has opted out of insurance prompts
if !settings.noHoleCard,
!settings.neverAskInsurance,
let upCard = dealerUpCard,
engine.shouldOfferInsurance(dealerUpCard: upCard) {
currentPhase = .insurance currentPhase = .insurance
return return
} }
@ -567,6 +571,12 @@ final class GameState {
} }
} }
/// Declines insurance and sets the "never ask again" preference.
func neverAskInsurance() {
settings.neverAskInsurance = true
declineInsurance()
}
// MARK: - Player Actions // MARK: - Player Actions
/// Player hits (takes another card). /// Player hits (takes another card).

View File

@ -105,6 +105,9 @@ final class GameSettings {
/// Whether insurance is offered. /// Whether insurance is offered.
var insuranceAllowed: Bool = true { didSet { save() } } var insuranceAllowed: Bool = true { didSet { save() } }
/// Whether to skip the insurance prompt and auto-decline.
var neverAskInsurance: Bool = false { didSet { save() } }
/// Blackjack payout ratio (1.5 = 3:2, 1.2 = 6:5) /// Blackjack payout ratio (1.5 = 3:2, 1.2 = 6:5)
var blackjackPayout: Double = 1.5 { didSet { save() } } var blackjackPayout: Double = 1.5 { didSet { save() } }
@ -237,6 +240,7 @@ final class GameSettings {
self.noHoleCard = data.noHoleCard self.noHoleCard = data.noHoleCard
self.blackjackPayout = data.blackjackPayout self.blackjackPayout = data.blackjackPayout
self.insuranceAllowed = data.insuranceAllowed self.insuranceAllowed = data.insuranceAllowed
self.neverAskInsurance = data.neverAskInsurance
self.sideBetsEnabled = data.sideBetsEnabled self.sideBetsEnabled = data.sideBetsEnabled
self.showAnimations = data.showAnimations self.showAnimations = data.showAnimations
self.dealingSpeed = data.dealingSpeed self.dealingSpeed = data.dealingSpeed
@ -263,6 +267,7 @@ final class GameSettings {
noHoleCard: noHoleCard, noHoleCard: noHoleCard,
blackjackPayout: blackjackPayout, blackjackPayout: blackjackPayout,
insuranceAllowed: insuranceAllowed, insuranceAllowed: insuranceAllowed,
neverAskInsurance: neverAskInsurance,
sideBetsEnabled: sideBetsEnabled, sideBetsEnabled: sideBetsEnabled,
showAnimations: showAnimations, showAnimations: showAnimations,
dealingSpeed: dealingSpeed, dealingSpeed: dealingSpeed,
@ -290,6 +295,7 @@ final class GameSettings {
noHoleCard = false noHoleCard = false
blackjackPayout = 1.5 blackjackPayout = 1.5
insuranceAllowed = true insuranceAllowed = true
neverAskInsurance = false
sideBetsEnabled = false sideBetsEnabled = false
showAnimations = true showAnimations = true
dealingSpeed = 1.0 dealingSpeed = 1.0

View File

@ -1057,6 +1057,28 @@
} }
} }
}, },
"Auto-decline when dealer shows Ace" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Auto-decline when dealer shows Ace"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rechazar automáticamente cuando el crupier muestra As"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Refuser automatiquement quand le croupier montre un As"
}
}
}
},
"Baccarat" : { "Baccarat" : {
"comment" : "The name of a casino game.", "comment" : "The name of a casino game.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@ -2625,6 +2647,28 @@
} }
} }
}, },
"Don't Ask" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Don't Ask"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "No preguntar"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ne pas demander"
}
}
}
},
"Double" : { "Double" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@ -5800,6 +5844,28 @@
} }
} }
}, },
"Skip Insurance Prompt" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Skip Insurance Prompt"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Omitir aviso de seguro"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignorer l'invitation d'assurance"
}
}
}
},
"Some games: Dealer hits on 'soft 17' (Ace + 6)." : { "Some games: Dealer hits on 'soft 17' (Ace + 6)." : {
"comment" : "Description of a rule where the dealer must hit on a 'soft 17' (Ace + 6) in some blackjack games.", "comment" : "Description of a rule where the dealer must hit on a 'soft 17' (Ace + 6) in some blackjack games.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,

View File

@ -66,6 +66,7 @@ struct BlackjackSettingsData: PersistableGameData {
noHoleCard: false, noHoleCard: false,
blackjackPayout: 1.5, blackjackPayout: 1.5,
insuranceAllowed: true, insuranceAllowed: true,
neverAskInsurance: false,
sideBetsEnabled: false, sideBetsEnabled: false,
showAnimations: true, showAnimations: true,
dealingSpeed: 1.0, dealingSpeed: 1.0,
@ -90,6 +91,7 @@ struct BlackjackSettingsData: PersistableGameData {
var noHoleCard: Bool var noHoleCard: Bool
var blackjackPayout: Double var blackjackPayout: Double
var insuranceAllowed: Bool var insuranceAllowed: Bool
var neverAskInsurance: Bool
var sideBetsEnabled: Bool var sideBetsEnabled: Bool
var showAnimations: Bool var showAnimations: Bool
var dealingSpeed: Double var dealingSpeed: Double

View File

@ -121,10 +121,15 @@ enum Design {
// MARK: - Card Deal Animation // MARK: - Card Deal Animation
enum DealAnimation { enum DealAnimation {
/// Horizontal offset for card deal (from upper-right, simulating shoe) /// Horizontal offset for dealer cards (shoe is nearby, less horizontal travel)
static let offsetX: CGFloat = 150 static let dealerOffsetX: CGFloat = 120
/// Vertical offset for card deal (from above the table) /// Vertical offset for dealer cards (small since near top)
static let offsetY: CGFloat = -200 static let dealerOffsetY: CGFloat = -80
/// Horizontal offset for player cards (shoe is far away, more horizontal travel)
static let playerOffsetX: CGFloat = 180
/// Vertical offset for player cards (large since far from top)
static let playerOffsetY: CGFloat = -350
} }
} }

View File

@ -186,7 +186,8 @@ struct GameTableView: View {
betAmount: state.currentBet / 2, betAmount: state.currentBet / 2,
balance: state.balance, balance: state.balance,
onTake: { Task { await state.takeInsurance() } }, onTake: { Task { await state.takeInsurance() } },
onDecline: { state.declineInsurance() } onDecline: { state.declineInsurance() },
onNeverAsk: { state.neverAskInsurance() }
) )
} }
.ignoresSafeArea() .ignoresSafeArea()

View File

@ -146,6 +146,15 @@ struct SettingsView: View {
isOn: $settings.showCardsRemaining, isOn: $settings.showCardsRemaining,
accentColor: accent accentColor: accent
) )
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Skip Insurance Prompt"),
subtitle: String(localized: "Auto-decline when dealer shows Ace"),
isOn: $settings.neverAskInsurance,
accentColor: accent
)
} }
} }

View File

@ -91,6 +91,8 @@ struct BlackjackTableView: View {
hand: state.dealerHand, hand: state.dealerHand,
showHoleCard: state.shouldShowDealerHoleCard, showHoleCard: state.shouldShowDealerHoleCard,
showCardCount: showCardCount, showCardCount: showCardCount,
showAnimations: state.settings.showAnimations,
dealingSpeed: state.settings.dealingSpeed,
cardWidth: cardWidth, cardWidth: cardWidth,
cardSpacing: cardSpacing cardSpacing: cardSpacing
) )
@ -121,6 +123,8 @@ struct BlackjackTableView: View {
activeHandIndex: state.activeHandIndex, activeHandIndex: state.activeHandIndex,
isPlayerTurn: state.isPlayerTurn, isPlayerTurn: state.isPlayerTurn,
showCardCount: showCardCount, showCardCount: showCardCount,
showAnimations: state.settings.showAnimations,
dealingSpeed: state.settings.dealingSpeed,
cardWidth: cardWidth, cardWidth: cardWidth,
cardSpacing: cardSpacing, cardSpacing: cardSpacing,
currentHint: state.currentHint, currentHint: state.currentHint,

View File

@ -12,12 +12,19 @@ struct DealerHandView: View {
let hand: BlackjackHand let hand: BlackjackHand
let showHoleCard: Bool let showHoleCard: Bool
let showCardCount: Bool let showCardCount: Bool
let showAnimations: Bool
let dealingSpeed: Double
let cardWidth: CGFloat let cardWidth: CGFloat
let cardSpacing: CGFloat let cardSpacing: CGFloat
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge @ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge
/// Scaled animation duration based on dealing speed.
private var animationDuration: Double {
Design.Animation.springDuration * dealingSpeed
}
var body: some View { var body: some View {
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
@ -62,12 +69,14 @@ struct DealerHandView: View {
} }
.zIndex(Double(index)) .zIndex(Double(index))
.transition( .transition(
.asymmetric( showAnimations
insertion: .offset(x: Design.DealAnimation.offsetX, y: Design.DealAnimation.offsetY) ? .asymmetric(
insertion: .offset(x: Design.DealAnimation.dealerOffsetX, y: Design.DealAnimation.dealerOffsetY)
.combined(with: .opacity) .combined(with: .opacity)
.combined(with: .scale(scale: Design.Scale.slightShrink)), .combined(with: .scale(scale: Design.Scale.slightShrink)),
removal: .scale.combined(with: .opacity) removal: .scale.combined(with: .opacity)
) )
: .identity
) )
} }
@ -78,7 +87,12 @@ struct DealerHandView: View {
} }
} }
} }
.animation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce), value: hand.cards.count) .animation(
showAnimations
? .spring(duration: animationDuration, bounce: Design.Animation.springBounce)
: .none,
value: hand.cards.count
)
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
// Result badge - overlayed so it doesn't add height to the view // Result badge - overlayed so it doesn't add height to the view
if let result = hand.cards.count >= 2 && showHoleCard ? handResultText : nil { if let result = hand.cards.count >= 2 && showHoleCard ? handResultText : nil {
@ -136,6 +150,8 @@ struct DealerHandView: View {
hand: BlackjackHand(), hand: BlackjackHand(),
showHoleCard: false, showHoleCard: false,
showCardCount: false, showCardCount: false,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60, cardWidth: 60,
cardSpacing: -20 cardSpacing: -20
) )
@ -152,6 +168,8 @@ struct DealerHandView: View {
]), ]),
showHoleCard: false, showHoleCard: false,
showCardCount: false, showCardCount: false,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60, cardWidth: 60,
cardSpacing: -20 cardSpacing: -20
) )
@ -168,6 +186,8 @@ struct DealerHandView: View {
]), ]),
showHoleCard: true, showHoleCard: true,
showCardCount: true, showCardCount: true,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60, cardWidth: 60,
cardSpacing: -20 cardSpacing: -20
) )

View File

@ -13,6 +13,7 @@ struct InsurancePopupView: View {
let balance: Int let balance: Int
let onTake: () -> Void let onTake: () -> Void
let onDecline: () -> Void let onDecline: () -> Void
let onNeverAsk: () -> Void
@State private var showContent = false @State private var showContent = false
@ -47,39 +48,50 @@ struct InsurancePopupView: View {
.padding(.bottom, Design.Spacing.small) .padding(.bottom, Design.Spacing.small)
// Buttons // Buttons
HStack(spacing: Design.Spacing.large) { VStack(spacing: Design.Spacing.medium) {
// Decline button HStack(spacing: Design.Spacing.large) {
Button(action: onDecline) { // Decline button
Text(String(localized: "No Thanks")) Button(action: onDecline) {
.font(.system(size: Design.BaseFontSize.medium, weight: .semibold)) Text(String(localized: "No Thanks"))
.foregroundStyle(.white) .font(.system(size: Design.BaseFontSize.medium, weight: .semibold))
.padding(.horizontal, Design.Spacing.xLarge) .foregroundStyle(.white)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(Color.red.opacity(Design.Opacity.heavy))
)
}
// Accept button (only if can afford)
if balance >= betAmount {
Button(action: onTake) {
Text(String(localized: "Yes ($\(betAmount))"))
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
.background( .background(
Capsule() Capsule()
.fill( .fill(Color.red.opacity(Design.Opacity.heavy))
LinearGradient(
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
) )
} }
// Accept button (only if can afford)
if balance >= betAmount {
Button(action: onTake) {
Text(String(localized: "Yes ($\(betAmount))"))
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(
LinearGradient(
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
)
}
}
}
// Don't Ask Again button
Button(action: onNeverAsk) {
Text(String(localized: "Don't Ask"))
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.small)
} }
} }
} }
@ -113,7 +125,8 @@ struct InsurancePopupView: View {
betAmount: 500, betAmount: 500,
balance: 4500, balance: 4500,
onTake: {}, onTake: {},
onDecline: {} onDecline: {},
onNeverAsk: {}
) )
} }
@ -122,7 +135,8 @@ struct InsurancePopupView: View {
betAmount: 500, betAmount: 500,
balance: 200, balance: 200,
onTake: {}, onTake: {},
onDecline: {} onDecline: {},
onNeverAsk: {}
) )
} }

View File

@ -16,6 +16,8 @@ struct PlayerHandsView: View {
let activeHandIndex: Int let activeHandIndex: Int
let isPlayerTurn: Bool let isPlayerTurn: Bool
let showCardCount: Bool let showCardCount: Bool
let showAnimations: Bool
let dealingSpeed: Double
let cardWidth: CGFloat let cardWidth: CGFloat
let cardSpacing: CGFloat let cardSpacing: CGFloat
@ -43,6 +45,8 @@ struct PlayerHandsView: View {
hand: hand, hand: hand,
isActive: isActiveHand, isActive: isActiveHand,
showCardCount: showCardCount, showCardCount: showCardCount,
showAnimations: showAnimations,
dealingSpeed: dealingSpeed,
// Hand numbers: rightmost (index 0) is Hand 1, played first // Hand numbers: rightmost (index 0) is Hand 1, played first
handNumber: hands.count > 1 ? index + 1 : nil, handNumber: hands.count > 1 ? index + 1 : nil,
cardWidth: cardWidth, cardWidth: cardWidth,
@ -95,6 +99,8 @@ struct PlayerHandView: View {
let hand: BlackjackHand let hand: BlackjackHand
let isActive: Bool let isActive: Bool
let showCardCount: Bool let showCardCount: Bool
let showAnimations: Bool
let dealingSpeed: Double
let handNumber: Int? let handNumber: Int?
let cardWidth: CGFloat let cardWidth: CGFloat
let cardSpacing: CGFloat let cardSpacing: CGFloat
@ -105,6 +111,11 @@ struct PlayerHandView: View {
/// Whether the hint toast should be visible. /// Whether the hint toast should be visible.
let showHintToast: Bool let showHintToast: Bool
/// Scaled animation duration based on dealing speed.
private var animationDuration: Double {
Design.Animation.springDuration * dealingSpeed
}
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize @ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.handIconSize @ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.handIconSize
@ -132,17 +143,24 @@ struct PlayerHandView: View {
} }
.zIndex(Double(index)) .zIndex(Double(index))
.transition( .transition(
.asymmetric( showAnimations
insertion: .offset(x: Design.DealAnimation.offsetX, y: Design.DealAnimation.offsetY) ? .asymmetric(
insertion: .offset(x: Design.DealAnimation.playerOffsetX, y: Design.DealAnimation.playerOffsetY)
.combined(with: .opacity) .combined(with: .opacity)
.combined(with: .scale(scale: Design.Scale.slightShrink)), .combined(with: .scale(scale: Design.Scale.slightShrink)),
removal: .scale.combined(with: .opacity) removal: .scale.combined(with: .opacity)
) )
: .identity
) )
} }
} }
} }
.animation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce), value: hand.cards.count) .animation(
showAnimations
? .spring(duration: animationDuration, bounce: Design.Animation.springBounce)
: .none,
value: hand.cards.count
)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
.background( .background(
@ -246,6 +264,8 @@ struct PlayerHandView: View {
activeHandIndex: 0, activeHandIndex: 0,
isPlayerTurn: true, isPlayerTurn: true,
showCardCount: false, showCardCount: false,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60, cardWidth: 60,
cardSpacing: -20, cardSpacing: -20,
currentHint: nil, currentHint: nil,
@ -265,6 +285,8 @@ struct PlayerHandView: View {
activeHandIndex: 0, activeHandIndex: 0,
isPlayerTurn: true, isPlayerTurn: true,
showCardCount: false, showCardCount: false,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60, cardWidth: 60,
cardSpacing: -20, cardSpacing: -20,
currentHint: "Hit", currentHint: "Hit",
@ -298,6 +320,8 @@ struct PlayerHandView: View {
activeHandIndex: 1, activeHandIndex: 1,
isPlayerTurn: true, isPlayerTurn: true,
showCardCount: true, showCardCount: true,
showAnimations: true,
dealingSpeed: 1.0,
cardWidth: 60, cardWidth: 60,
cardSpacing: -20, cardSpacing: -20,
currentHint: "Stand", currentHint: "Stand",