// // PlayerHandView.swift // Blackjack // // Displays player hands in a horizontally scrollable container. // import SwiftUI import CasinoKit // MARK: - Player Hands Container /// Container for multiple player hands with horizontal scrolling. struct PlayerHandsView: View { let hands: [BlackjackHand] let activeHandIndex: Int let isPlayerTurn: Bool let showCardCount: Bool let cardWidth: CGFloat let cardSpacing: CGFloat /// Total card count across all hands - used to trigger scroll when hitting private var totalCardCount: Int { hands.reduce(0) { $0 + $1.cards.count } } var body: some View { GeometryReader { geometry in ScrollViewReader { proxy in ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: Design.Spacing.large) { // Display hands in reverse order (right to left play order) // Visual order: Hand 3, Hand 2, Hand 1 (left to right) // Play order: Hand 1 played first (rightmost), then Hand 2, etc. ForEach(hands.indices.reversed(), id: \.self) { index in PlayerHandView( hand: hands[index], isActive: index == activeHandIndex && isPlayerTurn, showCardCount: showCardCount, // Hand numbers: rightmost (index 0) is Hand 1, played first handNumber: hands.count > 1 ? index + 1 : nil, cardWidth: cardWidth, cardSpacing: cardSpacing ) .id(index) } } .padding(.horizontal, Design.Spacing.large) .frame(minWidth: geometry.size.width) } .scrollClipDisabled() .scrollBounceBehavior(.basedOnSize) .onChange(of: activeHandIndex) { _, newIndex in scrollToHand(proxy: proxy, index: newIndex) } .onChange(of: totalCardCount) { _, _ in // Scroll to active hand when cards are added (hit) scrollToHand(proxy: proxy, index: activeHandIndex) } .onChange(of: hands.count) { _, _ in // Scroll to active hand when split occurs scrollToHand(proxy: proxy, index: activeHandIndex) } .onAppear { scrollToHand(proxy: proxy, index: activeHandIndex) } } } .frame(height: Design.Size.playerHandsHeight) } private func scrollToHand(proxy: ScrollViewProxy, index: Int) { withAnimation(.easeInOut(duration: Design.Animation.quick)) { proxy.scrollTo(index, anchor: .center) } } } // MARK: - Single Player Hand /// Displays a single player hand with cards, value, and result. struct PlayerHandView: View { let hand: BlackjackHand let isActive: Bool let showCardCount: Bool let handNumber: Int? let cardWidth: CGFloat let cardSpacing: CGFloat @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize @ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize var body: some View { VStack(spacing: Design.Spacing.small) { // Cards with container HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) { if hand.cards.isEmpty { CardPlaceholderView(width: cardWidth) CardPlaceholderView(width: cardWidth) } else { ForEach(hand.cards.indices, id: \.self) { index in CardView( card: hand.cards[index], isFaceUp: true, cardWidth: cardWidth ) .overlay(alignment: .bottomLeading) { if showCardCount { HiLoCountBadge(card: hand.cards[index]) } } .zIndex(Double(index)) } } } .padding(.horizontal, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.medium) .fill(Color.Table.feltDark.opacity(Design.Opacity.light)) .overlay( RoundedRectangle(cornerRadius: Design.CornerRadius.medium) .strokeBorder( isActive ? Color.Hand.active : Color.white.opacity(Design.Opacity.hint), lineWidth: isActive ? Design.LineWidth.thick : Design.LineWidth.thin ) ) ) .contentShape(Rectangle()) .animation(.easeInOut(duration: Design.Animation.quick), value: isActive) // Hand info HStack(spacing: Design.Spacing.small) { if let number = handNumber { Text(String(localized: "Hand \(number)")) .font(.system(size: handNumberSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } if !hand.cards.isEmpty { Text(hand.valueDisplay) .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) .foregroundStyle(valueColor) } if hand.isDoubledDown { Image(systemName: "xmark.circle.fill") .font(.system(size: Design.Size.handIconSize)) .foregroundStyle(.purple) } } // Result badge if let result = hand.result { Text(result.displayText) .font(.system(size: labelFontSize, weight: .black)) .foregroundStyle(result.color) .padding(.horizontal, Design.Size.hintPaddingH) .padding(.vertical, Design.Size.hintPaddingV) .background( Capsule() .fill(result.color.opacity(Design.Opacity.hint)) ) } // Bet amount if hand.bet > 0 { HStack(spacing: Design.Spacing.xSmall) { Image(systemName: "dollarsign.circle.fill") .font(.system(size: Design.Size.handIconSize)) .foregroundStyle(.yellow) Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))") .font(.system(size: handNumberSize, weight: .bold, design: .rounded)) .foregroundStyle(.yellow) } } } .accessibilityElement(children: .ignore) .accessibilityLabel(playerAccessibilityLabel) } // MARK: - Computed Properties private var valueColor: Color { if hand.isBlackjack { return .yellow } if hand.isBusted { return .red } if hand.value == 21 { return .green } return .white } private var playerAccessibilityLabel: String { let cardsDescription = hand.cards.map { $0.accessibilityDescription }.joined(separator: ", ") var label = String(localized: "Player hand: \(cardsDescription). Value: \(hand.valueDisplay)") if let result = hand.result { label += ". \(result.displayText)" } return label } } // MARK: - Previews #Preview("Single Hand - Empty") { ZStack { Color.Table.felt.ignoresSafeArea() PlayerHandsView( hands: [BlackjackHand()], activeHandIndex: 0, isPlayerTurn: true, showCardCount: false, cardWidth: 60, cardSpacing: -20 ) } } #Preview("Single Hand - Cards") { ZStack { Color.Table.felt.ignoresSafeArea() PlayerHandsView( hands: [BlackjackHand(cards: [ Card(suit: .clubs, rank: .eight), Card(suit: .hearts, rank: .nine) ], bet: 100)], activeHandIndex: 0, isPlayerTurn: true, showCardCount: false, cardWidth: 60, cardSpacing: -20 ) } } #Preview("Split Hands") { ZStack { Color.Table.felt.ignoresSafeArea() PlayerHandsView( hands: [ BlackjackHand(cards: [ Card(suit: .clubs, rank: .eight), Card(suit: .spades, rank: .jack) ], bet: 100), BlackjackHand(cards: [ Card(suit: .hearts, rank: .eight), Card(suit: .diamonds, rank: .five) ], bet: 100), BlackjackHand(cards: [ Card(suit: .hearts, rank: .eight), Card(suit: .diamonds, rank: .five) ], bet: 100), BlackjackHand(cards: [ Card(suit: .hearts, rank: .eight), Card(suit: .diamonds, rank: .five) ], bet: 100) ], activeHandIndex: 1, isPlayerTurn: true, showCardCount: true, cardWidth: 60, cardSpacing: -20 ) } }