Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
ca742eb73f
commit
04fc1542f5
@ -813,7 +813,13 @@ final class GameState: CasinoGameState {
|
||||
let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 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() {
|
||||
playerHands[activeHandIndex].cards.append(card1)
|
||||
sound.play(.cardDeal)
|
||||
@ -1274,6 +1280,144 @@ final class GameState: CasinoGameState {
|
||||
performResetGame()
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -2268,6 +2268,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Deal Splittable Pair (8s)" : {
|
||||
|
||||
},
|
||||
"DEALER" : {
|
||||
"localizations" : {
|
||||
|
||||
@ -26,6 +26,36 @@ struct SettingsView: View {
|
||||
/// Accent color for settings components
|
||||
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 {
|
||||
SheetContainerView(
|
||||
title: String(localized: "Settings"),
|
||||
@ -408,6 +438,38 @@ struct SettingsView: View {
|
||||
.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
|
||||
Text(appVersionString)
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
|
||||
@ -22,17 +22,28 @@ struct DealerHandView: View {
|
||||
|
||||
@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).
|
||||
private var displayValue: Int? {
|
||||
/// The value text to display in the badge (based on visible cards and hole card state).
|
||||
/// 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 }
|
||||
|
||||
if showHoleCard {
|
||||
// Hole card revealed - calculate value from visible cards
|
||||
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 {
|
||||
// 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
|
||||
HandLabelView(
|
||||
title: String(localized: "DEALER"),
|
||||
value: displayValue,
|
||||
valueText: displayValueText,
|
||||
badgeColor: Color.Hand.dealer
|
||||
)
|
||||
.animation(nil, value: visibleCardCount)
|
||||
|
||||
@ -61,7 +61,15 @@ struct PlayerHandView: View {
|
||||
let isBusted = hardValue > 21
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -80,9 +80,9 @@ struct PlayerHandsContainer: View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
handsContent
|
||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||
// Ensure minimum width to fill viewport, centering smaller content via layout
|
||||
// This avoids scroll anchor re-centering which doesn't animate
|
||||
.containerRelativeFrame(.horizontal, alignment: .center)
|
||||
// Only use containerRelativeFrame for single hand (centering)
|
||||
// For multiple hands, allow natural scrolling
|
||||
.modifier(CenterSingleHandModifier(isSingleHand: hands.count == 1))
|
||||
.scrollTargetLayout()
|
||||
}
|
||||
.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
|
||||
|
||||
#Preview("Single Hand") {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user