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

This commit is contained in:
Matt Bruce 2025-12-17 20:33:46 -06:00
parent a7d550eef1
commit 13010a3131
9 changed files with 304 additions and 27 deletions

View File

@ -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()

View File

@ -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()
}

View File

@ -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..<cardCount {
if let card = engine.dealCard() {
if i % 2 == 0 {
playerHands[0].cards.append(card)
@ -283,14 +291,24 @@ final class GameState {
}
}
// Check for insurance offer
if let upCard = dealerUpCard, engine.shouldOfferInsurance(dealerUpCard: upCard) {
// Check for insurance offer (only in American style with hole card)
if !settings.noHoleCard, let upCard = dealerUpCard, engine.shouldOfferInsurance(dealerUpCard: upCard) {
currentPhase = .insurance
return
}
// Check for immediate blackjacks
await checkForBlackjacks()
// Check for immediate blackjacks (only in American style - European checks after player acts)
if !settings.noHoleCard {
await checkForBlackjacks()
} else {
// European: just go to player turn (blackjacks checked after player acts)
if playerHands[0].isBlackjack {
// Player blackjack - will be handled after dealer gets second card
currentPhase = .playerTurn(handIndex: 0)
} else {
currentPhase = .playerTurn(handIndex: 0)
}
}
}
/// Checks for blackjacks and handles accordingly.
@ -491,12 +509,42 @@ final class GameState {
private func dealerTurn() async {
currentPhase = .dealerTurn
// Reveal hole card
sound.play(.cardFlip)
let delay = settings.showAnimations ? 0.5 * settings.dealingSpeed : 0
if delay > 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..<playerHands.count {
if playerHands[i].result == nil {
if playerHands[i].isBlackjack {
playerHands[i].result = .push
} else {
playerHands[i].result = .lose
}
}
}
await completeRound()
return
}
} else {
// American style: reveal hole card
sound.play(.cardFlip)
if delay > 0 {
try? await Task.sleep(for: .seconds(delay))
}
}
// Dealer draws

View File

@ -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,

View File

@ -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.",

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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.")
]
)
]

View File

@ -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") {