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 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2268,6 +2268,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Deal Splittable Pair (8s)" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"DEALER" : {
|
"DEALER" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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") {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user