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

This commit is contained in:
Matt Bruce 2025-12-31 10:27:43 -06:00
parent ca742eb73f
commit 04fc1542f5
6 changed files with 255 additions and 10 deletions

View File

@ -813,7 +813,13 @@ final class GameState: CasinoGameState {
let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 0 let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 0
let remainingDelay = settings.showAnimations ? animationDuration * 0.85 : 0 let remainingDelay = settings.showAnimations ? animationDuration * 0.85 : 0
// Deal one card to each hand // Brief delay to let SwiftUI render the split hands before dealing second cards
// This ensures both hand containers are visible before cards animate in
if settings.showAnimations {
try? await Task.sleep(for: .milliseconds(150))
}
// Deal one card to each hand (with full animation timing for each)
if let card1 = engine.dealCard() { if let card1 = engine.dealCard() {
playerHands[activeHandIndex].cards.append(card1) playerHands[activeHandIndex].cards.append(card1)
sound.play(.cardDeal) sound.play(.cardDeal)
@ -1274,6 +1280,144 @@ final class GameState: CasinoGameState {
performResetGame() performResetGame()
// Note: newRound() is called by resetForNewSession() // Note: newRound() is called by resetForNewSession()
} }
// MARK: - Debug Helpers
//
// DEBUG TESTING UTILITIES
//
//
// These methods are only available in DEBUG builds and provide ways to test
// specific game scenarios that are difficult to trigger with random card deals.
//
// ACCESS:
// - Settings sheet scroll to bottom "DEBUG" section (orange)
// - Only visible in DEBUG builds (not in Release/App Store builds)
//
// ADDING NEW DEBUG SCENARIOS:
// 1. Add a new async function below following the pattern of `debugDealWithPair()`
// 2. Add a corresponding button in SettingsView.swift inside the #if DEBUG block
// 3. Use `triggerDebugDeal(state:)` pattern to dismiss sheet before executing
//
// ANIMATION TIMING NOTES:
// - Always add 100ms delay after phase changes to let SwiftUI render containers
// - Use `cardAppearDelay` (15% of animation) before updating visible counts
// - Use `remainingDelay` (85% of animation) before dealing next card
// - For splits, add 150ms delay after creating hands before dealing second cards
//
//
#if DEBUG
/// Forces a deal with a splittable pair for testing split hand scrolling and animations.
///
/// This debug function deals a pair of 8s to the player (a classic split scenario)
/// with a dealer showing 6 (favorable split situation). Use this to test:
/// - Split hand scrolling behavior
/// - Card dealing animations for split hands
/// - PlayerHandsContainer centering with multiple hands
///
/// ## Usage
/// Triggered via Settings DEBUG "Deal Splittable Pair (8s)"
///
/// ## Dealt Cards
/// - Player: 8, 8 (pair, can split)
/// - Dealer: 6 (up), 10 (hole)
///
/// ## Notes
/// - Auto-places minimum bet if none exists
/// - Must be in betting phase to work
/// - Includes proper animation timing delays
func debugDealWithPair() async {
Design.debugLog("🧪 Debug deal started - phase: \(currentPhase), bet: \(currentBet)")
// Auto-place minimum bet if none exists
if currentBet < settings.minBet {
currentBet = settings.minBet
Design.debugLog("🧪 Auto-placed min bet: \(currentBet)")
}
guard currentPhase == .betting else {
Design.debugLog("🧪 Debug deal failed - not in betting phase (phase: \(currentPhase))")
return
}
Design.debugLog("🧪 Starting debug deal with pair of 8s")
currentPhase = .dealing
dealerHand = BlackjackHand()
activeHandIndex = 0
insuranceBet = 0
// Reset visible card counts
playerHandsVisibleCardCount = [0]
dealerVisibleCardCount = 0
// Brief delay to let PlayerHandsContainer appear before cards fly in
// (fixes race condition where first card animation is missed)
if settings.showAnimations {
try? await Task.sleep(for: .milliseconds(100))
}
// Create a pair of 8s (classic split scenario) with dealer showing 6
let card1 = Card(suit: .hearts, rank: .eight)
let card2 = Card(suit: .spades, rank: .eight)
let dealerCard1 = Card(suit: .diamonds, rank: .six)
let dealerCard2 = Card(suit: .clubs, rank: .ten)
playerHands = [BlackjackHand(cards: [], bet: currentBet)]
// Animation timing (matches deal() function)
let animationDuration = Design.Animation.springDuration * settings.dealingSpeed
let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 0
let remainingDelay = settings.showAnimations ? animationDuration * 0.85 : 0
// Deal player card 1
playerHands[0].cards.append(card1)
sound.play(.cardDeal)
if cardAppearDelay > 0 { try? await Task.sleep(for: .seconds(cardAppearDelay)) }
playerHandsVisibleCardCount[0] += 1
if remainingDelay > 0 { try? await Task.sleep(for: .seconds(remainingDelay)) }
// Deal dealer card 1 (face up)
dealerHand.cards.append(dealerCard1)
sound.play(.cardDeal)
if cardAppearDelay > 0 { try? await Task.sleep(for: .seconds(cardAppearDelay)) }
dealerVisibleCardCount += 1
if remainingDelay > 0 { try? await Task.sleep(for: .seconds(remainingDelay)) }
// Deal player card 2 (matching rank for split)
playerHands[0].cards.append(card2)
sound.play(.cardDeal)
if cardAppearDelay > 0 { try? await Task.sleep(for: .seconds(cardAppearDelay)) }
playerHandsVisibleCardCount[0] += 1
if remainingDelay > 0 { try? await Task.sleep(for: .seconds(remainingDelay)) }
// Deal dealer hole card (face down)
dealerHand.cards.append(dealerCard2)
sound.play(.cardDeal)
if cardAppearDelay > 0 { try? await Task.sleep(for: .seconds(cardAppearDelay)) }
dealerVisibleCardCount += 1
if remainingDelay > 0 { try? await Task.sleep(for: .seconds(remainingDelay)) }
currentPhase = .playerTurn(handIndex: 0)
Design.debugLog("🧪 Debug deal complete - pair of 8s, can split: \(canSplit)")
}
//
// ADD NEW DEBUG SCENARIOS BELOW
//
//
// Example template for a new debug scenario:
//
// /// Description of what this tests.
// func debugDealWithBlackjack() async {
// Design.debugLog("🧪 Debug blackjack started")
// // ... implementation following the pattern above
// }
//
// Then add a button in SettingsView.swift's DEBUG section.
//
#endif
} }

View File

@ -2268,6 +2268,9 @@
} }
} }
} }
},
"Deal Splittable Pair (8s)" : {
}, },
"DEALER" : { "DEALER" : {
"localizations" : { "localizations" : {

View File

@ -26,6 +26,36 @@ struct SettingsView: View {
/// Accent color for settings components /// Accent color for settings components
private let accent = Color.Sheet.accent private let accent = Color.Sheet.accent
//
// DEBUG HELPERS
//
//
// These methods trigger debug scenarios from GameState.
// The pattern is: dismiss sheet wait for animation call debug function
//
// TO ADD A NEW DEBUG TRIGGER:
// 1. Add a new trigger function below following the pattern
// 2. Add a corresponding button in the DEBUG SheetSection in body
// 3. The debug function itself lives in GameState.swift
//
//
#if DEBUG
/// Triggers the debug deal with splittable pair after dismissing the sheet.
/// Must dismiss first because the deal needs the main game view visible.
private func triggerDebugDeal(state: GameState) {
dismiss()
Task { @MainActor in
// Wait for sheet dismiss animation to complete
try? await Task.sleep(for: .milliseconds(500))
await state.debugDealWithPair()
}
}
// Add new trigger functions here following the same pattern:
// private func triggerDebugBlackjack(state: GameState) { ... }
#endif
var body: some View { var body: some View {
SheetContainerView( SheetContainerView(
title: String(localized: "Settings"), title: String(localized: "Settings"),
@ -408,6 +438,38 @@ struct SettingsView: View {
.padding(.top, Design.Spacing.xSmall) .padding(.top, Design.Spacing.xSmall)
} }
//
// DEBUG SECTION - Only visible in DEBUG builds
// Add new debug buttons here. Each button should call a
// trigger function that dismisses the sheet first, then
// calls the corresponding debug function in GameState.
//
#if DEBUG
if let state = gameState {
SheetSection(title: "DEBUG", icon: "ant.fill") {
// Split Testing - deals a pair of 8s
Button {
triggerDebugDeal(state: state)
} label: {
HStack {
Text("Deal Splittable Pair (8s)")
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.orange)
Spacer()
Image(systemName: "rectangle.split.2x1")
.font(.system(size: Design.BaseFontSize.large))
.foregroundStyle(.orange)
}
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
}
// Add new debug buttons here:
// Divider().background(Color.orange.opacity(Design.Opacity.hint))
// Button { triggerDebugBlackjack(state: state) } label: { ... }
}
}
#endif
// 13. Version info // 13. Version info
Text(appVersionString) Text(appVersionString)
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))

View File

@ -22,17 +22,28 @@ struct DealerHandView: View {
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
/// The value to display in the badge (based on visible cards and hole card state). /// The value text to display in the badge (based on visible cards and hole card state).
private var displayValue: Int? { /// Shows soft values like "7/17" for consistency with player hand display.
private var displayValueText: String? {
guard !hand.cards.isEmpty && visibleCardCount > 0 else { return nil } guard !hand.cards.isEmpty && visibleCardCount > 0 else { return nil }
if showHoleCard { if showHoleCard {
// Hole card revealed - calculate value from visible cards // Hole card revealed - calculate value from visible cards
let visibleCards = Array(hand.cards.prefix(visibleCardCount)) let visibleCards = Array(hand.cards.prefix(visibleCardCount))
return BlackjackHand.bestValue(for: visibleCards) let (hardValue, softValue) = BlackjackHand.calculateValues(for: visibleCards)
let hasSoftAce = BlackjackHand.hasSoftAce(for: visibleCards)
// When soft value is 21, there's no ambiguity - just show 21
if softValue == 21 {
return "21"
} else if hasSoftAce {
return "\(hardValue)/\(softValue)"
} else {
return "\(BlackjackHand.bestValue(for: visibleCards))"
}
} else { } else {
// Hole card hidden - show only the first (face-up) card's value // Hole card hidden - show only the first (face-up) card's value
return hand.cards[0].blackjackValue return "\(hand.cards[0].blackjackValue)"
} }
} }
@ -41,7 +52,7 @@ struct DealerHandView: View {
// Label and value badge // Label and value badge
HandLabelView( HandLabelView(
title: String(localized: "DEALER"), title: String(localized: "DEALER"),
value: displayValue, valueText: displayValueText,
badgeColor: Color.Hand.dealer badgeColor: Color.Hand.dealer
) )
.animation(nil, value: visibleCardCount) .animation(nil, value: visibleCardCount)

View File

@ -61,7 +61,15 @@ struct PlayerHandView: View {
let isBusted = hardValue > 21 let isBusted = hardValue > 21
// Show value like hand.valueDisplay does (e.g., "8/18" for soft hands) // Show value like hand.valueDisplay does (e.g., "8/18" for soft hands)
let valueText = hasSoftAce ? "\(hardValue)/\(softValue)" : "\(displayValue)" // When soft value is 21, there's no ambiguity - just show 21
let valueText: String
if softValue == 21 {
valueText = "21"
} else if hasSoftAce {
valueText = "\(hardValue)/\(softValue)"
} else {
valueText = "\(displayValue)"
}
return (valueText, displayValue, isBusted) return (valueText, displayValue, isBusted)
} }

View File

@ -80,9 +80,9 @@ struct PlayerHandsContainer: View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
handsContent handsContent
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
// Ensure minimum width to fill viewport, centering smaller content via layout // Only use containerRelativeFrame for single hand (centering)
// This avoids scroll anchor re-centering which doesn't animate // For multiple hands, allow natural scrolling
.containerRelativeFrame(.horizontal, alignment: .center) .modifier(CenterSingleHandModifier(isSingleHand: hands.count == 1))
.scrollTargetLayout() .scrollTargetLayout()
} }
.scrollClipDisabled() .scrollClipDisabled()
@ -113,6 +113,23 @@ struct PlayerHandsContainer: View {
} }
} }
// MARK: - Centering Modifier
/// Conditionally applies containerRelativeFrame for centering single hands.
/// For multiple hands, allows natural content width for scrolling.
private struct CenterSingleHandModifier: ViewModifier {
let isSingleHand: Bool
func body(content: Content) -> some View {
if isSingleHand {
content
.containerRelativeFrame(.horizontal, alignment: .center)
} else {
content
}
}
}
// MARK: - Previews // MARK: - Previews
#Preview("Single Hand") { #Preview("Single Hand") {