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

This commit is contained in:
Matt Bruce 2025-12-24 11:27:27 -06:00
parent 09770ec625
commit 7234cd718a
8 changed files with 240 additions and 67 deletions

View File

@ -2832,6 +2832,52 @@
} }
} }
}, },
"Reset Game" : {
"comment" : "A button label that resets the game balance and reshuffles the deck.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reset Game"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reiniciar juego"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Réinitialiser la partie"
}
}
}
},
"Restore starting balance and reshuffle" : {
"comment" : "Description for the reset game button explaining what it does.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Restore starting balance and reshuffle"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Restaurar saldo inicial y barajar"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Restaurer le solde initial et mélanger"
}
}
}
},
"Reset to Defaults" : { "Reset to Defaults" : {
"comment" : "A button label that resets game settings to their default values.", "comment" : "A button label that resets game settings to their default values.",
"localizations" : { "localizations" : {

View File

@ -135,7 +135,7 @@ struct GameTableView: View {
balance: state.balance, balance: state.balance,
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.shoe.cardsRemaining)" : nil, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.shoe.cardsRemaining)" : nil,
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil, secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
onReset: { state.resetGame() }, showReset: false,
onSettings: { showSettings = true }, onSettings: { showSettings = true },
onHelp: { showRules = true }, onHelp: { showRules = true },
onStats: { showStats = true } onStats: { showStats = true }
@ -256,7 +256,7 @@ struct GameTableView: View {
balance: state.balance, balance: state.balance,
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.shoe.cardsRemaining)" : nil, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.shoe.cardsRemaining)" : nil,
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil, secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
onReset: { state.resetGame() }, showReset: false,
onSettings: { showSettings = true }, onSettings: { showSettings = true },
onHelp: { showRules = true }, onHelp: { showRules = true },
onStats: { showStats = true } onStats: { showStats = true }

View File

@ -150,7 +150,7 @@ struct SettingsView: View {
} }
} }
.tint(accent) .tint(accent)
.padding(.vertical, Design.Spacing.xSmall) .frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
if gameState.iCloudEnabled { if gameState.iCloudEnabled {
Divider() Divider()
@ -173,6 +173,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
} }
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
Divider() Divider()
.background(Color.white.opacity(Design.Opacity.subtle)) .background(Color.white.opacity(Design.Opacity.subtle))
@ -181,11 +182,13 @@ struct SettingsView: View {
gameState.syncWithCloud() gameState.syncWithCloud()
} label: { } label: {
HStack { HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text(String(localized: "Sync Now")) Text(String(localized: "Sync Now"))
Spacer()
Image(systemName: "arrow.triangle.2.circlepath")
} }
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(accent) .foregroundStyle(accent)
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }
} else { } else {
@ -203,7 +206,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
} }
.padding(.vertical, Design.Spacing.xSmall) .frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }
@ -237,15 +240,43 @@ struct SettingsView: View {
Divider() Divider()
.background(Color.white.opacity(Design.Opacity.subtle)) .background(Color.white.opacity(Design.Opacity.subtle))
// Reset Game - resets balance, keeps stats
Button {
gameState.resetGame()
dismiss()
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "Reset Game"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
Text(String(localized: "Restore starting balance and reshuffle"))
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Spacer()
Image(systemName: "arrow.counterclockwise")
.font(.system(size: Design.BaseFontSize.large))
.foregroundStyle(Color.Sheet.accent)
}
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
}
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
// Clear All Data - nuclear option
Button(role: .destructive) { Button(role: .destructive) {
showClearDataAlert = true showClearDataAlert = true
} label: { } label: {
HStack { HStack {
Image(systemName: "trash")
Text(String(localized: "Clear All Data")) Text(String(localized: "Clear All Data"))
Spacer()
Image(systemName: "trash")
} }
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.red) .foregroundStyle(.red)
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }
@ -263,6 +294,7 @@ struct SettingsView: View {
.font(.system(size: Design.BaseFontSize.body)) .font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }

View File

@ -55,8 +55,8 @@
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "%1$@ bet, pays up to %2$@" "value": "%1$@ bet, pays up to %2$@"
} }
}, },
"es-MX": { "es-MX": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -656,7 +656,7 @@
"state": "translated", "state": "translated",
"value": "10-A: -1 (cartas altas favorecen al jugador)" "value": "10-A: -1 (cartas altas favorecen al jugador)"
} }
}, },
"fr-CA": { "fr-CA": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -1890,7 +1890,7 @@
"state": "translated", "state": "translated",
"value": "Borrar todos los datos" "value": "Borrar todos los datos"
} }
}, },
"fr-CA": { "fr-CA": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -2900,7 +2900,7 @@
"state": "translated", "state": "translated",
"value": "Double tap to add chips" "value": "Double tap to add chips"
} }
}, },
"es-MX": { "es-MX": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -3063,7 +3063,7 @@
"state": "translated", "state": "translated",
"value": "Ejemplo: K♠ K♠ = Par perfecto" "value": "Ejemplo: K♠ K♠ = Par perfecto"
} }
}, },
"fr-CA": { "fr-CA": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -4080,7 +4080,7 @@
"state": "translated", "state": "translated",
"value": "LEGAL" "value": "LEGAL"
} }
}, },
"es-MX": { "es-MX": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -4693,7 +4693,7 @@
"state": "translated", "state": "translated",
"value": "Option 2: Use IconRenderer in Code" "value": "Option 2: Use IconRenderer in Code"
} }
}, },
"es-MX": { "es-MX": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -4715,7 +4715,7 @@
"state": "translated", "state": "translated",
"value": "Optional bets placed before the deal." "value": "Optional bets placed before the deal."
} }
}, },
"es-MX": { "es-MX": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -5434,6 +5434,52 @@
} }
} }
}, },
"Reset Game": {
"comment": "Button to reset game balance and reshuffle cards.",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Reset Game"
}
},
"es-MX": {
"stringUnit": {
"state": "translated",
"value": "Reiniciar Juego"
}
},
"fr-CA": {
"stringUnit": {
"state": "translated",
"value": "Réinitialiser le Jeu"
}
}
}
},
"Restore starting balance and reshuffle": {
"comment": "Subtitle for Reset Game button explaining what it does.",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Restore starting balance and reshuffle"
}
},
"es-MX": {
"stringUnit": {
"state": "translated",
"value": "Restaurar saldo inicial y barajar"
}
},
"fr-CA": {
"stringUnit": {
"state": "translated",
"value": "Restaurer le solde initial et remélanger"
}
}
}
},
"Running Count: Sum of all card values seen.": { "Running Count: Sum of all card values seen.": {
"localizations": { "localizations": {
"en": { "en": {
@ -5875,7 +5921,7 @@
"state": "translated", "state": "translated",
"value": "Divide hasta 4 manos, pero no ases." "value": "Divide hasta 4 manos, pero no ases."
} }
}, },
"fr-CA": { "fr-CA": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -5892,13 +5938,13 @@
"state": "translated", "state": "translated",
"value": "Split, not Stand (TC %@)" "value": "Split, not Stand (TC %@)"
} }
}, },
"es-MX": { "es-MX": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "Dividir, no Plantarse (TC %@)" "value": "Dividir, no Plantarse (TC %@)"
} }
}, },
"fr-CA": { "fr-CA": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",

View File

@ -97,7 +97,7 @@ struct GameTableView: View {
balance: state.balance, balance: state.balance,
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil,
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil, secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
onReset: { state.resetGame() }, showReset: false,
onSettings: { showSettings = true }, onSettings: { showSettings = true },
onHelp: { showRules = true }, onHelp: { showRules = true },
onStats: { showStats = true } onStats: { showStats = true }
@ -105,16 +105,6 @@ struct GameTableView: View {
.frame(maxWidth: maxContentWidth) .frame(maxWidth: maxContentWidth)
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar") .debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
// Card count display (when enabled)
if settings.showCardCount {
CardCountView(
runningCount: state.engine.runningCount,
trueCount: state.engine.trueCount
)
.frame(maxWidth: maxContentWidth)
.debugBorder(showDebugBorders, color: .mint, label: "CardCount")
}
// Reshuffle notification // Reshuffle notification
if state.showReshuffleNotification { if state.showReshuffleNotification {
ReshuffleNotificationView(showCardCount: settings.showCardCount) ReshuffleNotificationView(showCardCount: settings.showCardCount)
@ -164,16 +154,16 @@ struct GameTableView: View {
if state.currentPhase == .insurance { if state.currentPhase == .insurance {
Color.clear Color.clear
.overlay(alignment: .center) { .overlay(alignment: .center) {
InsurancePopupView( InsurancePopupView(
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() }
) )
} }
.ignoresSafeArea() .ignoresSafeArea()
.allowsHitTesting(true) .allowsHitTesting(true)
.transition(.opacity.combined(with: .scale(scale: 0.9))) .transition(.opacity.combined(with: .scale(scale: 0.9)))
.zIndex(100) .zIndex(100)
} }
@ -181,13 +171,13 @@ struct GameTableView: View {
if state.showResultBanner, let result = state.lastRoundResult { if state.showResultBanner, let result = state.lastRoundResult {
Color.clear Color.clear
.overlay(alignment: .center) { .overlay(alignment: .center) {
ResultBannerView( ResultBannerView(
result: result, result: result,
currentBalance: state.balance, currentBalance: state.balance,
minBet: state.settings.minBet, minBet: state.settings.minBet,
onNewRound: { state.newRound() }, onNewRound: { state.newRound() },
onPlayAgain: { state.resetGame() } onPlayAgain: { state.resetGame() }
) )
.onAppear { .onAppear {
Design.debugLog("🎯 RESULT BANNER APPEARED") Design.debugLog("🎯 RESULT BANNER APPEARED")
} }
@ -210,11 +200,11 @@ struct GameTableView: View {
if state.isGameOver && !state.showResultBanner { if state.isGameOver && !state.showResultBanner {
Color.clear Color.clear
.overlay(alignment: .center) { .overlay(alignment: .center) {
GameOverView( GameOverView(
roundsPlayed: state.roundsPlayed, roundsPlayed: state.roundsPlayed,
onPlayAgain: { state.resetGame() } onPlayAgain: { state.resetGame() }
) )
} }
.ignoresSafeArea() .ignoresSafeArea()
.allowsHitTesting(true) .allowsHitTesting(true)
.zIndex(100) .zIndex(100)

View File

@ -204,7 +204,7 @@ struct SettingsView: View {
} }
} }
.tint(accent) .tint(accent)
.padding(.vertical, Design.Spacing.xSmall) .frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
if state.persistence.iCloudEnabled { if state.persistence.iCloudEnabled {
Divider() Divider()
@ -227,6 +227,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
} }
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
Divider() Divider()
.background(Color.white.opacity(Design.Opacity.subtle)) .background(Color.white.opacity(Design.Opacity.subtle))
@ -235,11 +236,13 @@ struct SettingsView: View {
state.persistence.sync() state.persistence.sync()
} label: { } label: {
HStack { HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text(String(localized: "Sync Now")) Text(String(localized: "Sync Now"))
Spacer()
Image(systemName: "arrow.triangle.2.circlepath")
} }
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(accent) .foregroundStyle(accent)
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }
} else { } else {
@ -257,7 +260,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
} }
.padding(.vertical, Design.Spacing.xSmall) .frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }
} }
@ -287,21 +290,50 @@ struct SettingsView: View {
Text(winnings >= 0 ? "+$\(winnings)" : "-$\(abs(winnings))") Text(winnings >= 0 ? "+$\(winnings)" : "-$\(abs(winnings))")
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded)) .font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
.foregroundStyle(winnings >= 0 ? .green : .red) .foregroundStyle(winnings >= 0 ? .green : .red)
} }
} }
Divider() Divider()
.background(Color.white.opacity(Design.Opacity.subtle)) .background(Color.white.opacity(Design.Opacity.subtle))
// Reset Game - resets balance, keeps stats
Button {
state.resetGame()
dismiss()
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "Reset Game"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
Text(String(localized: "Restore starting balance and reshuffle"))
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Spacer()
Image(systemName: "arrow.counterclockwise")
.font(.system(size: Design.BaseFontSize.large))
.foregroundStyle(Color.Sheet.accent)
}
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
}
Divider()
.background(Color.white.opacity(Design.Opacity.subtle))
// Clear All Data - nuclear option
Button(role: .destructive) { Button(role: .destructive) {
showClearDataAlert = true showClearDataAlert = true
} label: { } label: {
HStack { HStack {
Image(systemName: "trash")
Text(String(localized: "Clear All Data")) Text(String(localized: "Clear All Data"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
Spacer()
Image(systemName: "trash")
.font(.system(size: Design.BaseFontSize.large))
} }
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.red) .foregroundStyle(.red)
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }
} }
@ -320,6 +352,7 @@ struct SettingsView: View {
.font(.system(size: Design.BaseFontSize.body)) .font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
} }
} }

View File

@ -48,7 +48,7 @@ struct BlackjackTableView: View {
/// Card width based on full screen height (stable - doesn't change with content) /// Card width based on full screen height (stable - doesn't change with content)
private var cardWidth: CGFloat { private var cardWidth: CGFloat {
let maxDimension = screenHeight let maxDimension = screenHeight
let percentage: CGFloat = 0.13 // ~10% of screen let percentage: CGFloat = 0.15 // ~10% of screen
return maxDimension * percentage return maxDimension * percentage
} }
@ -93,21 +93,44 @@ struct BlackjackTableView: View {
) )
.debugBorder(showDebugBorders, color: .red, label: "Dealer") .debugBorder(showDebugBorders, color: .red, label: "Dealer")
// Flexible space between dealer and player - scales with screen size // Space between dealer and player - contains card count if enabled
Spacer(minLength: dealerPlayerSpacing) if showCardCount {
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer \(Int(dealerPlayerSpacing))") // Card count view fills the space between hands
VStack {
// Top spacer for larger screens
if screenHeight > 700 {
Spacer(minLength: Design.Spacing.small)
}
CardCountView(
runningCount: state.engine.runningCount,
trueCount: state.engine.trueCount
)
// Bottom spacer for larger screens
if screenHeight > 700 {
Spacer(minLength: Design.Spacing.small)
}
}
.frame(minHeight: dealerPlayerSpacing)
.debugBorder(showDebugBorders, color: .mint, label: "CardCount")
} else {
// No card count - just use flexible spacer
Spacer(minLength: dealerPlayerSpacing)
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer \(Int(dealerPlayerSpacing))")
}
// Player hands area - only show when there are cards dealt // Player hands area - only show when there are cards dealt
if state.playerHands.first?.cards.isEmpty == false { if state.playerHands.first?.cards.isEmpty == false {
ZStack { ZStack {
PlayerHandsView( PlayerHandsView(
hands: state.playerHands, hands: state.playerHands,
activeHandIndex: state.activeHandIndex, activeHandIndex: state.activeHandIndex,
isPlayerTurn: state.isPlayerTurn, isPlayerTurn: state.isPlayerTurn,
showCardCount: showCardCount, showCardCount: showCardCount,
cardWidth: cardWidth, cardWidth: cardWidth,
cardSpacing: cardSpacing cardSpacing: cardSpacing
) )
// Side bet toasts (positioned on left/right sides to not cover cards) // Side bet toasts (positioned on left/right sides to not cover cards)
if state.settings.sideBetsEnabled && state.showSideBetToasts { if state.settings.sideBetsEnabled && state.showSideBetToasts {

View File

@ -147,6 +147,9 @@ public enum CasinoDesign {
/// Checkmark size. /// Checkmark size.
public static let checkmark: CGFloat = 22 public static let checkmark: CGFloat = 22
/// Minimum height for tappable action rows (Apple HIG: 44pt minimum touch target).
public static let actionRowMinHeight: CGFloat = 44
/// Common button dimensions. /// Common button dimensions.
public static let actionButtonHeight: CGFloat = 50 public static let actionButtonHeight: CGFloat = 50
public static var actionButtonMinWidth: CGFloat { public static var actionButtonMinWidth: CGFloat {