diff --git a/Baccarat/Views/GameTableView.swift b/Baccarat/Views/GameTableView.swift index 4bcd986..a43fc8e 100644 --- a/Baccarat/Views/GameTableView.swift +++ b/Baccarat/Views/GameTableView.swift @@ -802,7 +802,7 @@ struct ActionButtonsView: View { .disabled(gameState.currentBets.isEmpty) } else { Button("Clear", systemImage: "xmark.circle", action: onClear) - .labelStyle(.titleOnly) + .labelStyle(.titleAndIcon) .font(.system(size: buttonFontSize, weight: .semibold)) .foregroundStyle(.white) .padding(.horizontal, Design.Spacing.xxLarge) @@ -839,10 +839,10 @@ struct ActionButtonsView: View { .disabled(!gameState.canDeal) } else { Button("Deal", systemImage: "play.fill", action: onDeal) - .labelStyle(.titleOnly) + .labelStyle(.titleAndIcon) .font(.system(size: buttonFontSize, weight: .bold)) .foregroundStyle(.black) - .padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small) + .padding(.horizontal, Design.Spacing.xxLarge) .padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall) .background( Capsule() @@ -863,7 +863,7 @@ struct ActionButtonsView: View { @ViewBuilder private var newRoundButton: some View { if isAccessibilitySize { - Button("New Round", systemImage: "arrow.right.circle", action: onNewRound) + Button("New Round", systemImage: "arrow.clockwise", action: onNewRound) .labelStyle(.iconOnly) .font(.system(size: iconSize, weight: .bold)) .foregroundStyle(.black) @@ -880,11 +880,11 @@ struct ActionButtonsView: View { ) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) } else { - Button("New Round", systemImage: "arrow.right.circle", action: onNewRound) - .labelStyle(.titleOnly) + Button("New Round", systemImage: "arrow.clockwise", action: onNewRound) + .labelStyle(.titleAndIcon) .font(.system(size: buttonFontSize, weight: .bold)) .foregroundStyle(.black) - .padding(.horizontal, Design.Spacing.xxxLarge + Design.Spacing.small) + .padding(.horizontal, Design.Spacing.xxLarge) .padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall) .background( Capsule() diff --git a/Blackjack/Engine/BlackjackEngine.swift b/Blackjack/Engine/BlackjackEngine.swift index a996279..f078eb8 100644 --- a/Blackjack/Engine/BlackjackEngine.swift +++ b/Blackjack/Engine/BlackjackEngine.swift @@ -17,12 +17,14 @@ final class BlackjackEngine { /// The card shoe. private(set) var shoe: Deck - /// Number of decks in the shoe. - let deckCount: Int - /// Settings reference for rule variations. private let settings: GameSettings + /// Number of decks in the shoe (reads from current settings). + var deckCount: Int { + settings.deckCount.rawValue + } + /// Cards remaining in shoe. var cardsRemaining: Int { shoe.cardsRemaining @@ -38,16 +40,15 @@ final class BlackjackEngine { init(settings: GameSettings) { self.settings = settings - self.deckCount = settings.deckCount.rawValue - self.shoe = Deck(deckCount: deckCount) + self.shoe = Deck(deckCount: settings.deckCount.rawValue) shoe.shuffle() } // MARK: - Shoe Management - /// Reshuffles the shoe. + /// Reshuffles the shoe with the current deck count from settings. func reshuffle() { - shoe = Deck(deckCount: deckCount) + shoe = Deck(deckCount: settings.deckCount.rawValue) shoe.shuffle() } diff --git a/Blackjack/Engine/GameState.swift b/Blackjack/Engine/GameState.swift index e8d5690..4b3c7a1 100644 --- a/Blackjack/Engine/GameState.swift +++ b/Blackjack/Engine/GameState.swift @@ -168,6 +168,11 @@ final class GameState { sound.volume = settings.soundVolume } + /// Called when deck count setting changes - reshuffles with new deck count. + func applyDeckCountChange() { + engine.reshuffle() + } + // MARK: - Persistence /// Loads saved game data from iCloud or local storage. @@ -268,8 +273,11 @@ final class GameState { let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0 - // Deal cards: player, dealer, player, dealer - for i in 0..<4 { + // European no-hole-card: deal 3 cards (player, dealer, player) + // American style: deal 4 cards (player, dealer, player, dealer) + let cardCount = settings.noHoleCard ? 3 : 4 + + for i in 0.. 0 { - try? await Task.sleep(for: .seconds(delay)) + + // European no-hole-card: deal the second card now + if settings.noHoleCard && dealerHand.cards.count == 1 { + if let card = engine.dealCard() { + dealerHand.cards.append(card) + sound.play(.cardDeal) + + if delay > 0 { + try? await Task.sleep(for: .seconds(delay)) + } + } + + // Check for dealer blackjack in European mode + // Player loses everything (no early check in European) + if dealerHand.isBlackjack { + // Mark player hands as lost if they don't have blackjack + for i in 0.. 0 { + try? await Task.sleep(for: .seconds(delay)) + } } // Dealer draws diff --git a/Blackjack/Models/GameSettings.swift b/Blackjack/Models/GameSettings.swift index 5f327e7..05e4fda 100644 --- a/Blackjack/Models/GameSettings.swift +++ b/Blackjack/Models/GameSettings.swift @@ -99,6 +99,9 @@ final class GameSettings { /// Whether late surrender is allowed. var lateSurrender: Bool = true { didSet { save() } } + /// Whether European no-hole-card rule is used (dealer gets second card after player acts). + var noHoleCard: Bool = false { didSet { save() } } + /// Whether insurance is offered. var insuranceAllowed: Bool = true { didSet { save() } } @@ -171,6 +174,7 @@ final class GameSettings { doubleAfterSplit = true resplitAces = false lateSurrender = false + noHoleCard = false // American: dealer gets hole card upfront blackjackPayout = 1.5 case .atlantic: @@ -179,6 +183,7 @@ final class GameSettings { doubleAfterSplit = true resplitAces = true lateSurrender = true + noHoleCard = false // American: dealer gets hole card upfront blackjackPayout = 1.5 case .european: @@ -187,6 +192,7 @@ final class GameSettings { doubleAfterSplit = true resplitAces = false lateSurrender = false + noHoleCard = true // European: dealer gets second card after player acts blackjackPayout = 1.5 case .custom: @@ -220,6 +226,7 @@ final class GameSettings { self.doubleAfterSplit = data.doubleAfterSplit self.resplitAces = data.resplitAces self.lateSurrender = data.lateSurrender + self.noHoleCard = data.noHoleCard self.blackjackPayout = data.blackjackPayout self.insuranceAllowed = data.insuranceAllowed self.showAnimations = data.showAnimations @@ -243,6 +250,7 @@ final class GameSettings { doubleAfterSplit: doubleAfterSplit, resplitAces: resplitAces, lateSurrender: lateSurrender, + noHoleCard: noHoleCard, blackjackPayout: blackjackPayout, insuranceAllowed: insuranceAllowed, showAnimations: showAnimations, diff --git a/Blackjack/Resources/Localizable.xcstrings b/Blackjack/Resources/Localizable.xcstrings index df733a4..a0c9a21 100644 --- a/Blackjack/Resources/Localizable.xcstrings +++ b/Blackjack/Resources/Localizable.xcstrings @@ -38,10 +38,34 @@ "• Use an online tool to generate all sizes" : { "comment" : "A step in the process of exporting app icons.", "isCommentAutoGenerated" : true + }, + "1 Deck: Lowest house edge (~0.17%), rare to find." : { + + }, + "2 Decks: Low house edge (~0.35%), common online." : { + }, "2-10: Face value" : { "comment" : "Description of the card values for cards with values 2 through 10.", "isCommentAutoGenerated" : true + }, + "4 Decks: Moderate house edge (~0.45%)." : { + + }, + "6 decks shuffled together." : { + + }, + "6 Decks: Standard in Vegas (~0.50%)." : { + + }, + "6:5 Blackjack (avoid!): Increases house edge by ~1.4%." : { + + }, + "8 decks shuffled together." : { + + }, + "8 Decks: Standard in Atlantic City (~0.55%)." : { + }, "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.", @@ -128,6 +152,9 @@ "Alternative: Use an online tool" : { "comment" : "A section header that suggests using an online tool to generate app icons.", "isCommentAutoGenerated" : true + }, + "Always split Aces and 8s." : { + }, "An Ace + 10-value card dealt initially is 'Blackjack'." : { "comment" : "Description of a blackjack hand.", @@ -188,6 +215,9 @@ } } } + }, + "Basic Strategy" : { + }, "Basic strategy suggestions" : { "localizations" : { @@ -306,6 +336,9 @@ "Blackjack pays 3:2 (1.5x your bet)." : { "comment" : "Description of the payout for blackjack in the Blackjack rules help view.", "isCommentAutoGenerated" : true + }, + "Blackjack pays 3:2." : { + }, "Blackjack: 3:2" : { "comment" : "Payout description for a Blackjack win.", @@ -648,6 +681,9 @@ } } } + }, + "Dealer Hits Soft 17: Increases house edge by ~0.2%." : { + }, "Dealer must hit on 16 or less." : { "comment" : "Description of the dealer's rule to hit if the hand value is 16 or less.", @@ -700,6 +736,12 @@ } } } + }, + "Dealer stands on all 17s (including soft 17)." : { + + }, + "Dealer stands on all 17s." : { + }, "Dealer stands on soft 17, double after split, 3:2 blackjack" : { "comment" : "Description of the \"Vegas Strip\" blackjack rule variation.", @@ -724,6 +766,9 @@ "Dealer: No cards" : { "comment" : "Accessibility label for the dealer hand when there are no cards visible.", "isCommentAutoGenerated" : true + }, + "Deck Count" : { + }, "DECK SETTINGS" : { "localizations" : { @@ -834,6 +879,15 @@ } } } + }, + "Double after split (DAS) allowed." : { + + }, + "Double After Split (DAS): Reduces house edge by ~0.15%." : { + + }, + "Double after split allowed." : { + }, "Double Down" : { "localizations" : { @@ -856,10 +910,25 @@ } } } + }, + "Double down allowed on any two cards." : { + + }, + "Double down on any two cards." : { + }, "Double Down: Double your bet, take one card, then stand" : { "comment" : "Action available in Blackjack when the player wants to double their bet, take one more card, and then stand.", "isCommentAutoGenerated" : true + }, + "Double on 9, 10, or 11 only (some venues)." : { + + }, + "Double on 10 vs dealer 2-9." : { + + }, + "Double on 11 vs dealer 2-10." : { + }, "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.", @@ -886,6 +955,9 @@ } } } + }, + "Fewer decks favor the player slightly." : { + }, "GAME STYLE" : { "localizations" : { @@ -990,6 +1062,9 @@ } } } + }, + "Higher house edge due to no hole card." : { + }, "Hint: %@" : { "localizations" : { @@ -1034,6 +1109,9 @@ } } } + }, + "Hit on soft 17 or less." : { + }, "Hit: Take another card" : { "comment" : "Action available in Blackjack: Hit (take another card).", @@ -1202,6 +1280,12 @@ } } } + }, + "Late surrender available." : { + + }, + "Late Surrender: Reduces house edge by ~0.07%." : { + }, "Launch" : { "comment" : "A tab in the BrandingPreviewView that links to the launch screen preview.", @@ -1328,6 +1412,12 @@ } } } + }, + "More decks = harder to count cards." : { + + }, + "Most popular style on the Las Vegas Strip." : { + }, "Net" : { "localizations" : { @@ -1350,6 +1440,9 @@ } } } + }, + "Never split 10s or 5s." : { + }, "NEW GAME" : { "localizations" : { @@ -1438,6 +1531,12 @@ } } } + }, + "No hole card: dealer takes second card after player acts." : { + + }, + "No surrender option." : { + }, "Objective" : { "localizations" : { @@ -1635,6 +1734,12 @@ } } } + }, + "Re-split aces allowed." : { + + }, + "Re-split Aces: Reduces house edge by ~0.05%." : { + }, "Roulette" : { "comment" : "The name of a roulette card.", @@ -1665,6 +1770,9 @@ } } } + }, + "Rule Variations" : { + }, "RULES" : { "localizations" : { @@ -1893,6 +2001,9 @@ } } } + }, + "Split up to 4 hands, but not aces." : { + }, "Split: If you have two cards of the same value, split into two hands" : { "comment" : "Description of the 'Split' action in the game rules.", @@ -1919,6 +2030,9 @@ } } } + }, + "Stand on 17+ always." : { + }, "Stand: Keep your current hand" : { "comment" : "Action to keep your current hand in Blackjack.", @@ -1927,6 +2041,9 @@ "Standard casino" : { "comment" : "Description of a deck count option when the user selects 6 decks.", "isCommentAutoGenerated" : true + }, + "Standard rules on the East Coast." : { + }, "Statistics" : { "localizations" : { @@ -1993,6 +2110,9 @@ } } } + }, + "Surrender 16 vs dealer 9, 10, Ace." : { + }, "Surrender after dealer checks for blackjack" : { "localizations" : { @@ -2093,6 +2213,9 @@ "These show how the same pattern works for other games" : { "comment" : "A description below the section of the view that previews icons for other games.", "isCommentAutoGenerated" : true + }, + "Traditional European casino style." : { + }, "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.", diff --git a/Blackjack/Storage/BlackjackGameData.swift b/Blackjack/Storage/BlackjackGameData.swift index 79e1d9a..110a6ae 100644 --- a/Blackjack/Storage/BlackjackGameData.swift +++ b/Blackjack/Storage/BlackjackGameData.swift @@ -63,6 +63,7 @@ struct BlackjackSettingsData: PersistableGameData { doubleAfterSplit: true, resplitAces: false, lateSurrender: true, + noHoleCard: false, blackjackPayout: 1.5, insuranceAllowed: true, showAnimations: true, @@ -84,6 +85,7 @@ struct BlackjackSettingsData: PersistableGameData { var doubleAfterSplit: Bool var resplitAces: Bool var lateSurrender: Bool + var noHoleCard: Bool var blackjackPayout: Double var insuranceAllowed: Bool var showAnimations: Bool diff --git a/Blackjack/Views/BlackjackTableView.swift b/Blackjack/Views/BlackjackTableView.swift index bd5de08..64a5c41 100644 --- a/Blackjack/Views/BlackjackTableView.swift +++ b/Blackjack/Views/BlackjackTableView.swift @@ -120,8 +120,14 @@ struct DealerHandView: View { .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) .foregroundStyle(.white) - if !hand.cards.isEmpty && showHoleCard { - ValueBadge(value: hand.value, color: Color.Hand.dealer) + // Show value: always show if hole card visible, or show single card value in European mode + if !hand.cards.isEmpty { + if showHoleCard { + ValueBadge(value: hand.value, color: Color.Hand.dealer) + } else if hand.cards.count == 1 { + // European mode: show single visible card value + ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer) + } } } @@ -140,6 +146,12 @@ struct DealerHandView: View { ) .zIndex(Double(index)) } + + // Show placeholder for second card in European mode (no hole card) + if hand.cards.count == 1 && !showHoleCard { + CardPlaceholderView(width: cardWidth) + .opacity(Design.Opacity.medium) + } } } diff --git a/Blackjack/Views/RulesHelpView.swift b/Blackjack/Views/RulesHelpView.swift index 4e71d7d..5ebfc79 100644 --- a/Blackjack/Views/RulesHelpView.swift +++ b/Blackjack/Views/RulesHelpView.swift @@ -81,6 +81,85 @@ struct RulesHelpView: View { String(localized: "Push: Bet returned"), String(localized: "Surrender: Half bet returned") ] + ), + RulePage( + title: String(localized: "Vegas Strip"), + icon: "sparkles", + content: [ + String(localized: "Most popular style on the Las Vegas Strip."), + String(localized: "6 decks shuffled together."), + String(localized: "Dealer stands on all 17s (including soft 17)."), + String(localized: "Double down allowed on any two cards."), + String(localized: "Double after split (DAS) allowed."), + String(localized: "Split up to 4 hands, but not aces."), + String(localized: "No surrender option."), + String(localized: "Blackjack pays 3:2.") + ] + ), + RulePage( + title: String(localized: "Atlantic City"), + icon: "building.2.fill", + content: [ + String(localized: "Standard rules on the East Coast."), + String(localized: "8 decks shuffled together."), + String(localized: "Dealer stands on all 17s."), + String(localized: "Double down on any two cards."), + String(localized: "Double after split allowed."), + String(localized: "Re-split aces allowed."), + String(localized: "Late surrender available."), + String(localized: "Blackjack pays 3:2.") + ] + ), + RulePage( + title: String(localized: "European"), + icon: "globe.europe.africa.fill", + content: [ + String(localized: "Traditional European casino style."), + String(localized: "6 decks shuffled together."), + String(localized: "No hole card: dealer takes second card after player acts."), + String(localized: "Dealer stands on all 17s."), + String(localized: "Double on 9, 10, or 11 only (some venues)."), + String(localized: "Double after split allowed."), + String(localized: "No surrender option."), + String(localized: "Higher house edge due to no hole card.") + ] + ), + RulePage( + title: String(localized: "Deck Count"), + icon: "rectangle.stack.fill", + content: [ + String(localized: "1 Deck: Lowest house edge (~0.17%), rare to find."), + String(localized: "2 Decks: Low house edge (~0.35%), common online."), + String(localized: "4 Decks: Moderate house edge (~0.45%)."), + String(localized: "6 Decks: Standard in Vegas (~0.50%)."), + String(localized: "8 Decks: Standard in Atlantic City (~0.55%)."), + String(localized: "More decks = harder to count cards."), + String(localized: "Fewer decks favor the player slightly.") + ] + ), + RulePage( + title: String(localized: "Rule Variations"), + icon: "slider.horizontal.3", + content: [ + String(localized: "Dealer Hits Soft 17: Increases house edge by ~0.2%."), + String(localized: "Double After Split (DAS): Reduces house edge by ~0.15%."), + String(localized: "Re-split Aces: Reduces house edge by ~0.05%."), + String(localized: "Late Surrender: Reduces house edge by ~0.07%."), + String(localized: "6:5 Blackjack (avoid!): Increases house edge by ~1.4%.") + ] + ), + RulePage( + title: String(localized: "Basic Strategy"), + icon: "lightbulb.fill", + content: [ + String(localized: "Always split Aces and 8s."), + String(localized: "Never split 10s or 5s."), + String(localized: "Double on 11 vs dealer 2-10."), + String(localized: "Double on 10 vs dealer 2-9."), + String(localized: "Stand on 17+ always."), + String(localized: "Hit on soft 17 or less."), + String(localized: "Surrender 16 vs dealer 9, 10, Ace.") + ] ) ] diff --git a/Blackjack/Views/SettingsView.swift b/Blackjack/Views/SettingsView.swift index bb35344..e241ba7 100644 --- a/Blackjack/Views/SettingsView.swift +++ b/Blackjack/Views/SettingsView.swift @@ -27,6 +27,10 @@ struct SettingsView: View { SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") { DeckCountPicker(selection: $settings.deckCount) } + .onChange(of: settings.deckCount) { _, _ in + // Reshuffle with new deck count + gameState?.applyDeckCountChange() + } // Table Limits SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {