diff --git a/Baccarat/Baccarat/Engine/GameState.swift b/Baccarat/Baccarat/Engine/GameState.swift index d783f1b..9dbadcc 100644 --- a/Baccarat/Baccarat/Engine/GameState.swift +++ b/Baccarat/Baccarat/Engine/GameState.swift @@ -994,13 +994,10 @@ final class GameState: CasinoGameState { // MARK: - Game Reset /// Resets the entire game (keeps statistics). + /// Uses CasinoKit's performResetGame() which properly handles session ending. func resetGame() { - balance = settings.startingBalance - roundHistory = [] - engine = BaccaratEngine(deckCount: settings.deckCount.rawValue) - startNewSession() - newRoundInternal() - saveGameData() + performResetGame() + // Note: newRoundInternal() is called by resetForNewSession() // Play new game sound sound.playNewRound() diff --git a/Blackjack/Blackjack/Engine/GameState.swift b/Blackjack/Blackjack/Engine/GameState.swift index 6dea790..5dfe86f 100644 --- a/Blackjack/Blackjack/Engine/GameState.swift +++ b/Blackjack/Blackjack/Engine/GameState.swift @@ -518,7 +518,12 @@ final class GameState: CasinoGameState { playerHandsVisibleCardCount = [0] dealerVisibleCardCount = 0 - let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0 + // Animation timing + let animationDuration = Design.Animation.springDuration * settings.dealingSpeed + // Small delay for card to appear on screen before updating badge (~15% of animation) + let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 0 + // Remaining delay after badge update to complete the animation + let remainingDelay = settings.showAnimations ? animationDuration * 0.85 : 0 // European no-hole-card: deal 3 cards (player, dealer, player) // American style: deal 4 cards (player, dealer, player, dealer) @@ -532,16 +537,23 @@ final class GameState: CasinoGameState { dealerHand.cards.append(card) } sound.play(.cardDeal) - if delay > 0 { - try? await Task.sleep(for: .seconds(delay)) + + // Wait for card to appear on screen + if cardAppearDelay > 0 { + try? await Task.sleep(for: .seconds(cardAppearDelay)) } - // Mark card as visible after animation delay + // Now mark card as visible (badge updates) if i % 2 == 0 { playerHandsVisibleCardCount[0] += 1 } else { dealerVisibleCardCount += 1 } + + // Wait for remaining animation before dealing next card + if remainingDelay > 0 { + try? await Task.sleep(for: .seconds(remainingDelay)) + } } } @@ -660,15 +672,24 @@ final class GameState: CasinoGameState { playerHands[activeHandIndex].cards.append(card) sound.play(.cardDeal) - // Wait for animation if enabled - let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0 - if delay > 0 { - try? await Task.sleep(for: .seconds(delay)) + // Animation timing + let animationDuration = Design.Animation.springDuration * settings.dealingSpeed + let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 0 + let remainingDelay = settings.showAnimations ? animationDuration * 0.85 : 0 + + // Wait for card to appear on screen + if cardAppearDelay > 0 { + try? await Task.sleep(for: .seconds(cardAppearDelay)) } - // Mark card as visible after animation + // Mark card as visible (badge updates) playerHandsVisibleCardCount[activeHandIndex] += 1 + // Wait for remaining animation before processing result + if remainingDelay > 0 { + try? await Task.sleep(for: .seconds(remainingDelay)) + } + // Check for bust or 21 if playerHands[activeHandIndex].isBusted { playerHands[activeHandIndex].result = .bust @@ -717,14 +738,23 @@ final class GameState: CasinoGameState { playerHands[activeHandIndex].cards.append(card) sound.play(.cardDeal) - // Wait for animation if enabled - let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0 - if delay > 0 { - try? await Task.sleep(for: .seconds(delay)) + // Animation timing + let animationDuration = Design.Animation.springDuration * settings.dealingSpeed + let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 0 + let remainingDelay = settings.showAnimations ? animationDuration * 0.85 : 0 + + // Wait for card to appear on screen + if cardAppearDelay > 0 { + try? await Task.sleep(for: .seconds(cardAppearDelay)) } - // Mark card as visible after animation + // Mark card as visible (badge updates) playerHandsVisibleCardCount[activeHandIndex] += 1 + + // Wait for remaining animation + if remainingDelay > 0 { + try? await Task.sleep(for: .seconds(remainingDelay)) + } } if playerHands[activeHandIndex].isBusted { @@ -762,34 +792,47 @@ final class GameState: CasinoGameState { balance -= originalHand.bet sound.play(.chipPlace) - // Deal one card to each hand - let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0 - - if let card1 = engine.dealCard() { - hand1.cards.append(card1) - sound.play(.cardDeal) - if delay > 0 { - try? await Task.sleep(for: .seconds(delay)) - } - } - - if let card2 = engine.dealCard() { - hand2.cards.append(card2) - sound.play(.cardDeal) - if delay > 0 { - try? await Task.sleep(for: .seconds(delay)) - } - } - - // Replace original with split hands + // Replace original with split hands first (so visible counts are tracked correctly) playerHands.remove(at: activeHandIndex) playerHands.insert(hand1, at: activeHandIndex) playerHands.insert(hand2, at: activeHandIndex + 1) - // Update visible card counts - each split hand starts with 2 visible cards + // Each split hand starts with 1 visible card (the original cards) playerHandsVisibleCardCount.remove(at: activeHandIndex) - playerHandsVisibleCardCount.insert(2, at: activeHandIndex) - playerHandsVisibleCardCount.insert(2, at: activeHandIndex + 1) + playerHandsVisibleCardCount.insert(1, at: activeHandIndex) + playerHandsVisibleCardCount.insert(1, at: activeHandIndex + 1) + + // Animation timing + let animationDuration = Design.Animation.springDuration * settings.dealingSpeed + let cardAppearDelay = settings.showAnimations ? animationDuration * 0.15 : 0 + let remainingDelay = settings.showAnimations ? animationDuration * 0.85 : 0 + + // Deal one card to each hand + if let card1 = engine.dealCard() { + playerHands[activeHandIndex].cards.append(card1) + sound.play(.cardDeal) + + if cardAppearDelay > 0 { + try? await Task.sleep(for: .seconds(cardAppearDelay)) + } + playerHandsVisibleCardCount[activeHandIndex] += 1 + if remainingDelay > 0 { + try? await Task.sleep(for: .seconds(remainingDelay)) + } + } + + if let card2 = engine.dealCard() { + playerHands[activeHandIndex + 1].cards.append(card2) + sound.play(.cardDeal) + + if cardAppearDelay > 0 { + try? await Task.sleep(for: .seconds(cardAppearDelay)) + } + playerHandsVisibleCardCount[activeHandIndex + 1] += 1 + if remainingDelay > 0 { + try? await Task.sleep(for: .seconds(remainingDelay)) + } + } // If split aces, typically only one card each and stand if originalHand.cards[0].rank == .ace && !settings.resplitAces { @@ -848,20 +891,24 @@ final class GameState: CasinoGameState { private func dealerTurn() async { currentPhase = .dealerTurn - let delay = settings.showAnimations ? 0.5 * settings.dealingSpeed : 0 + // Animation timing + let animationDuration = Design.Animation.springDuration * settings.dealingSpeed + let delay = settings.showAnimations ? animationDuration : 0 + // For flip animation, card face becomes visible halfway through (at 90° rotation) + let flipMidpointDelay = settings.showAnimations ? animationDuration / 2.0 : 0 // 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) + // Mark card as visible immediately - face is visible as soon as card appears + dealerVisibleCardCount += 1 sound.play(.cardDeal) + // Wait for animation to complete before checking blackjack if delay > 0 { try? await Task.sleep(for: .seconds(delay)) } - - // Mark card as visible after animation - dealerVisibleCardCount += 1 } // Check for dealer blackjack in European mode @@ -881,31 +928,38 @@ final class GameState: CasinoGameState { return } } else { - // American style: reveal hole card (card is already in hand, just mark as visible) + // American style: reveal hole card (card is already in hand) + // The flip animation shows the card face at the midpoint (90° rotation) sound.play(.cardFlip) - if delay > 0 { - try? await Task.sleep(for: .seconds(delay)) + // Wait until card face becomes visible (halfway through flip) + if flipMidpointDelay > 0 { + try? await Task.sleep(for: .seconds(flipMidpointDelay)) } - // Mark hole card as visible (if not already) + // Mark hole card as visible now that card face is showing if dealerVisibleCardCount < dealerHand.cards.count { dealerVisibleCardCount = dealerHand.cards.count } + + // Wait for remaining flip animation to complete before drawing more cards + if flipMidpointDelay > 0 { + try? await Task.sleep(for: .seconds(flipMidpointDelay)) + } } // Dealer draws while engine.dealerShouldHit(hand: dealerHand) { if let card = engine.dealCard() { dealerHand.cards.append(card) + // Mark card as visible immediately - face is visible as soon as card appears + dealerVisibleCardCount += 1 sound.play(.cardDeal) + // Wait for animation to complete before drawing next card if delay > 0 { try? await Task.sleep(for: .seconds(delay)) } - - // Mark card as visible after animation - dealerVisibleCardCount += 1 } } @@ -1209,13 +1263,10 @@ final class GameState: CasinoGameState { // MARK: - Game Reset /// Resets the entire game (keeps statistics). + /// Uses CasinoKit's performResetGame() which properly handles session ending. func resetGame() { - balance = settings.startingBalance - roundHistory = [] - engine.reshuffle() - startNewSession() - newRound() - saveGameData() + performResetGame() + // Note: newRound() is called by resetForNewSession() } } diff --git a/CasinoKit/Sources/CasinoKit/Models/CasinoGameState.swift b/CasinoKit/Sources/CasinoKit/Models/CasinoGameState.swift index 2ce1fe6..7ca36a8 100644 --- a/CasinoKit/Sources/CasinoKit/Models/CasinoGameState.swift +++ b/CasinoKit/Sources/CasinoKit/Models/CasinoGameState.swift @@ -126,3 +126,38 @@ public extension CasinoGameState { } } +// MARK: - Game Reset Extensions + +public extension CasinoGameState { + /// Default implementation of resetGame that properly handles session ending. + /// Games can override this but should call the helper methods in the correct order. + /// + /// The correct order is: + /// 1. End current session (captures actual ending balance) + /// 2. Reset balance to starting balance + /// 3. Reset game-specific state (via resetForNewSession) + /// 4. Start new session with fresh balance + /// 5. Save data + func performResetGame() { + // 1. End current session FIRST while balance still shows actual state + if currentSession != nil { + endCurrentSession(reason: .manualEnd) + } + + // 2. Reset balance to starting balance + balance = startingBalance + + // 3. Let game reset its specific state (reshuffle deck, clear history, etc.) + resetForNewSession() + + // 4. Create new session with fresh balance + currentSession = GameSession( + gameStyle: currentGameStyle, + startingBalance: balance + ) + + // 5. Save + saveGameData() + } +} +