diff --git a/Baccarat/Baccarat/Resources/Localizable.xcstrings b/Baccarat/Baccarat/Resources/Localizable.xcstrings index 4b917c2..549fd4a 100644 --- a/Baccarat/Baccarat/Resources/Localizable.xcstrings +++ b/Baccarat/Baccarat/Resources/Localizable.xcstrings @@ -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" : { "comment" : "A button label that resets game settings to their default values.", "localizations" : { diff --git a/Baccarat/Baccarat/Views/Game/GameTableView.swift b/Baccarat/Baccarat/Views/Game/GameTableView.swift index b27b353..9a913ff 100644 --- a/Baccarat/Baccarat/Views/Game/GameTableView.swift +++ b/Baccarat/Baccarat/Views/Game/GameTableView.swift @@ -135,7 +135,7 @@ struct GameTableView: View { balance: state.balance, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.shoe.cardsRemaining)" : nil, secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil, - onReset: { state.resetGame() }, + showReset: false, onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } @@ -256,7 +256,7 @@ struct GameTableView: View { balance: state.balance, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.shoe.cardsRemaining)" : nil, secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil, - onReset: { state.resetGame() }, + showReset: false, onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } diff --git a/Baccarat/Baccarat/Views/Sheets/SettingsView.swift b/Baccarat/Baccarat/Views/Sheets/SettingsView.swift index ad4976b..5d775b3 100644 --- a/Baccarat/Baccarat/Views/Sheets/SettingsView.swift +++ b/Baccarat/Baccarat/Views/Sheets/SettingsView.swift @@ -150,7 +150,7 @@ struct SettingsView: View { } } .tint(accent) - .padding(.vertical, Design.Spacing.xSmall) + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) if gameState.iCloudEnabled { Divider() @@ -173,6 +173,7 @@ struct SettingsView: View { .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) Divider() .background(Color.white.opacity(Design.Opacity.subtle)) @@ -181,11 +182,13 @@ struct SettingsView: View { gameState.syncWithCloud() } label: { HStack { - Image(systemName: "arrow.triangle.2.circlepath") Text(String(localized: "Sync Now")) + Spacer() + Image(systemName: "arrow.triangle.2.circlepath") } .font(.system(size: Design.BaseFontSize.body, weight: .medium)) .foregroundStyle(accent) + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) } } } else { @@ -203,7 +206,7 @@ struct SettingsView: View { .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } - .padding(.vertical, Design.Spacing.xSmall) + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) } } @@ -237,15 +240,43 @@ struct SettingsView: View { Divider() .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) { showClearDataAlert = true } label: { HStack { - Image(systemName: "trash") Text(String(localized: "Clear All Data")) + Spacer() + Image(systemName: "trash") } .font(.system(size: Design.BaseFontSize.body, weight: .medium)) .foregroundStyle(.red) + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) } } @@ -263,6 +294,7 @@ struct SettingsView: View { .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) } } diff --git a/Blackjack/Blackjack/Resources/Localizable.xcstrings b/Blackjack/Blackjack/Resources/Localizable.xcstrings index eee7c12..c02c1e8 100644 --- a/Blackjack/Blackjack/Resources/Localizable.xcstrings +++ b/Blackjack/Blackjack/Resources/Localizable.xcstrings @@ -55,8 +55,8 @@ "stringUnit": { "state": "translated", "value": "%1$@ bet, pays up to %2$@" - } - }, + } + }, "es-MX": { "stringUnit": { "state": "translated", @@ -656,7 +656,7 @@ "state": "translated", "value": "10-A: -1 (cartas altas favorecen al jugador)" } - }, + }, "fr-CA": { "stringUnit": { "state": "translated", @@ -1890,7 +1890,7 @@ "state": "translated", "value": "Borrar todos los datos" } - }, + }, "fr-CA": { "stringUnit": { "state": "translated", @@ -2900,7 +2900,7 @@ "state": "translated", "value": "Double tap to add chips" } - }, + }, "es-MX": { "stringUnit": { "state": "translated", @@ -3063,7 +3063,7 @@ "state": "translated", "value": "Ejemplo: K♠ K♠ = Par perfecto" } - }, + }, "fr-CA": { "stringUnit": { "state": "translated", @@ -4080,7 +4080,7 @@ "state": "translated", "value": "LEGAL" } - }, + }, "es-MX": { "stringUnit": { "state": "translated", @@ -4693,7 +4693,7 @@ "state": "translated", "value": "Option 2: Use IconRenderer in Code" } - }, + }, "es-MX": { "stringUnit": { "state": "translated", @@ -4715,7 +4715,7 @@ "state": "translated", "value": "Optional bets placed before the deal." } - }, + }, "es-MX": { "stringUnit": { "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.": { "localizations": { "en": { @@ -5875,7 +5921,7 @@ "state": "translated", "value": "Divide hasta 4 manos, pero no ases." } - }, + }, "fr-CA": { "stringUnit": { "state": "translated", @@ -5892,13 +5938,13 @@ "state": "translated", "value": "Split, not Stand (TC %@)" } - }, + }, "es-MX": { "stringUnit": { "state": "translated", "value": "Dividir, no Plantarse (TC %@)" } - }, + }, "fr-CA": { "stringUnit": { "state": "translated", diff --git a/Blackjack/Blackjack/Views/Game/GameTableView.swift b/Blackjack/Blackjack/Views/Game/GameTableView.swift index b55c72e..06f93ce 100644 --- a/Blackjack/Blackjack/Views/Game/GameTableView.swift +++ b/Blackjack/Blackjack/Views/Game/GameTableView.swift @@ -97,7 +97,7 @@ struct GameTableView: View { balance: state.balance, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil, secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil, - onReset: { state.resetGame() }, + showReset: false, onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } @@ -105,16 +105,6 @@ struct GameTableView: View { .frame(maxWidth: maxContentWidth) .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 if state.showReshuffleNotification { ReshuffleNotificationView(showCardCount: settings.showCardCount) @@ -164,16 +154,16 @@ struct GameTableView: View { if state.currentPhase == .insurance { Color.clear .overlay(alignment: .center) { - InsurancePopupView( - betAmount: state.currentBet / 2, - balance: state.balance, - onTake: { Task { await state.takeInsurance() } }, - onDecline: { state.declineInsurance() } - ) + InsurancePopupView( + betAmount: state.currentBet / 2, + balance: state.balance, + onTake: { Task { await state.takeInsurance() } }, + onDecline: { state.declineInsurance() } + ) } .ignoresSafeArea() .allowsHitTesting(true) - .transition(.opacity.combined(with: .scale(scale: 0.9))) + .transition(.opacity.combined(with: .scale(scale: 0.9))) .zIndex(100) } @@ -181,13 +171,13 @@ struct GameTableView: View { if state.showResultBanner, let result = state.lastRoundResult { Color.clear .overlay(alignment: .center) { - ResultBannerView( - result: result, - currentBalance: state.balance, - minBet: state.settings.minBet, - onNewRound: { state.newRound() }, - onPlayAgain: { state.resetGame() } - ) + ResultBannerView( + result: result, + currentBalance: state.balance, + minBet: state.settings.minBet, + onNewRound: { state.newRound() }, + onPlayAgain: { state.resetGame() } + ) .onAppear { Design.debugLog("🎯 RESULT BANNER APPEARED") } @@ -210,11 +200,11 @@ struct GameTableView: View { if state.isGameOver && !state.showResultBanner { Color.clear .overlay(alignment: .center) { - GameOverView( - roundsPlayed: state.roundsPlayed, - onPlayAgain: { state.resetGame() } - ) - } + GameOverView( + roundsPlayed: state.roundsPlayed, + onPlayAgain: { state.resetGame() } + ) + } .ignoresSafeArea() .allowsHitTesting(true) .zIndex(100) diff --git a/Blackjack/Blackjack/Views/Sheets/SettingsView.swift b/Blackjack/Blackjack/Views/Sheets/SettingsView.swift index 3933a9b..fd68e1a 100644 --- a/Blackjack/Blackjack/Views/Sheets/SettingsView.swift +++ b/Blackjack/Blackjack/Views/Sheets/SettingsView.swift @@ -204,7 +204,7 @@ struct SettingsView: View { } } .tint(accent) - .padding(.vertical, Design.Spacing.xSmall) + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) if state.persistence.iCloudEnabled { Divider() @@ -227,6 +227,7 @@ struct SettingsView: View { .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) Divider() .background(Color.white.opacity(Design.Opacity.subtle)) @@ -235,11 +236,13 @@ struct SettingsView: View { state.persistence.sync() } label: { HStack { - Image(systemName: "arrow.triangle.2.circlepath") Text(String(localized: "Sync Now")) + Spacer() + Image(systemName: "arrow.triangle.2.circlepath") } .font(.system(size: Design.BaseFontSize.body, weight: .medium)) .foregroundStyle(accent) + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) } } } else { @@ -257,7 +260,7 @@ struct SettingsView: View { .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))") .font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded)) .foregroundStyle(winnings >= 0 ? .green : .red) - } + } } Divider() .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) { showClearDataAlert = true } label: { HStack { - Image(systemName: "trash") 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) + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) } } } @@ -320,6 +352,7 @@ struct SettingsView: View { .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } + .frame(minHeight: CasinoDesign.Size.actionRowMinHeight) } } diff --git a/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift b/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift index 60d467b..ae74c8a 100644 --- a/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift +++ b/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift @@ -47,8 +47,8 @@ struct BlackjackTableView: View { /// Card width based on full screen height (stable - doesn't change with content) private var cardWidth: CGFloat { - let maxDimension = screenHeight - let percentage: CGFloat = 0.13 // ~10% of screen + let maxDimension = screenHeight + let percentage: CGFloat = 0.15 // ~10% of screen return maxDimension * percentage } @@ -93,21 +93,44 @@ struct BlackjackTableView: View { ) .debugBorder(showDebugBorders, color: .red, label: "Dealer") - // Flexible space between dealer and player - scales with screen size - Spacer(minLength: dealerPlayerSpacing) - .debugBorder(showDebugBorders, color: .yellow, label: "Spacer \(Int(dealerPlayerSpacing))") + // Space between dealer and player - contains card count if enabled + if showCardCount { + // 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 if state.playerHands.first?.cards.isEmpty == false { ZStack { - PlayerHandsView( - hands: state.playerHands, - activeHandIndex: state.activeHandIndex, - isPlayerTurn: state.isPlayerTurn, - showCardCount: showCardCount, - cardWidth: cardWidth, - cardSpacing: cardSpacing - ) + PlayerHandsView( + hands: state.playerHands, + activeHandIndex: state.activeHandIndex, + isPlayerTurn: state.isPlayerTurn, + showCardCount: showCardCount, + cardWidth: cardWidth, + cardSpacing: cardSpacing + ) // Side bet toasts (positioned on left/right sides to not cover cards) if state.settings.sideBetsEnabled && state.showSideBetToasts { diff --git a/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift b/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift index 7ee91b6..d996c87 100644 --- a/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift +++ b/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift @@ -147,6 +147,9 @@ public enum CasinoDesign { /// Checkmark size. 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. public static let actionButtonHeight: CGFloat = 50 public static var actionButtonMinWidth: CGFloat {