// // GameTableView.swift // Blackjack // // Main game container view. // import SwiftUI import CasinoKit struct GameTableView: View { @State private var settings = GameSettings() @State private var gameState: GameState? @State private var selectedChip: ChipDenomination = .twentyFive // MARK: - Sheet State @State private var showSettings = false @State private var showRules = false @State private var showStats = false // MARK: - Environment @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass /// Whether we're on iPad private var isIPad: Bool { horizontalSizeClass == .regular } /// Maximum content width based on device private var maxContentWidth: CGFloat { if isIPad { return verticalSizeClass == .compact ? Design.Size.maxContentWidthLandscape : Design.Size.maxContentWidthPortrait } return .infinity } // MARK: - Body var body: some View { Group { if let state = gameState { mainGameView(state: state) } else { ProgressView() .task { gameState = GameState(settings: settings) } } } .sheet(isPresented: $showSettings) { SettingsView(settings: settings, gameState: gameState) } .sheet(isPresented: $showRules) { RulesHelpView() } .sheet(isPresented: $showStats) { if let state = gameState { StatisticsSheetView(state: state) } } } // MARK: - Main Game View @ViewBuilder private func mainGameView(state: GameState) -> some View { ZStack { // Background TableBackgroundView() VStack(spacing: 0) { // Top bar TopBarView( balance: state.balance, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil, secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil, onReset: { state.resetGame() }, onSettings: { showSettings = true }, onHelp: { showRules = true }, onStats: { showStats = true } ) .frame(maxWidth: maxContentWidth) // Table layout BlackjackTableView( state: state, onPlaceBet: { placeBet(state: state) } ) .frame(maxWidth: maxContentWidth) Spacer() // Chip selector ChipSelectorView( selectedChip: $selectedChip, balance: state.balance, currentBet: state.currentBet, maxBet: state.settings.maxBet ) .frame(maxWidth: maxContentWidth) .padding(.bottom, Design.Spacing.small) // Action buttons ActionButtonsView(state: state) .frame(maxWidth: maxContentWidth) .padding(.bottom, Design.Spacing.medium) } .frame(maxWidth: .infinity) // Result banner overlay if state.showResultBanner, let result = state.lastRoundResult { ResultBannerView( result: result, currentBalance: state.balance, minBet: state.settings.minBet, onNewRound: { state.newRound() }, onPlayAgain: { state.resetGame() } ) } // Confetti for wins (matching Baccarat pattern) if state.showResultBanner && (state.lastRoundResult?.totalWinnings ?? 0) > 0 { ConfettiView() } // Game over if state.isGameOver && !state.showResultBanner { GameOverView( roundsPlayed: state.roundsPlayed, onPlayAgain: { state.resetGame() } ) } } } // MARK: - Betting private func placeBet(state: GameState) { state.placeBet(amount: selectedChip.rawValue) } } // MARK: - Action Buttons View struct ActionButtonsView: View { @Bindable var state: GameState // Scaled metrics @ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large @ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large // Fixed height to prevent layout shifts private let containerHeight: CGFloat = 120 var body: some View { ZStack { Color.clear .frame(height: containerHeight) VStack(spacing: Design.Spacing.medium) { // Primary actions HStack(spacing: Design.Spacing.medium) { switch state.currentPhase { case .betting: bettingButtons case .playerTurn: playerTurnButtons case .roundComplete: // Empty - handled by result banner EmptyView() default: // Dealing, dealer turn - show nothing EmptyView() } } .animation(.spring(duration: Design.Animation.quick), value: state.currentPhase) } } .padding(.horizontal, Design.Spacing.large) } // MARK: - Betting Phase Buttons @ViewBuilder private var bettingButtons: some View { if state.currentBet > 0 { ActionButton( String(localized: "Clear"), icon: "xmark.circle", style: .destructive ) { state.clearBet() } if state.canDeal { ActionButton( String(localized: "Deal"), icon: "play.fill", style: .primary ) { Task { await state.deal() } } } } } // MARK: - Player Turn Buttons @ViewBuilder private var playerTurnButtons: some View { // Top row: Hit, Stand HStack(spacing: Design.Spacing.medium) { if state.canHit { ActionButton( String(localized: "Hit"), style: .custom(Color.Button.hit) ) { Task { await state.hit() } } } if state.canStand { ActionButton( String(localized: "Stand"), style: .custom(Color.Button.stand) ) { Task { await state.stand() } } } } // Bottom row: Double, Split, Surrender HStack(spacing: Design.Spacing.medium) { if state.canDouble { ActionButton( String(localized: "Double"), style: .custom(Color.Button.doubleDown) ) { Task { await state.doubleDown() } } } if state.canSplit { ActionButton( String(localized: "Split"), style: .custom(Color.Button.split) ) { Task { await state.split() } } } if state.canSurrender { ActionButton( String(localized: "Surrender"), style: .custom(Color.Button.surrender) ) { Task { await state.surrender() } } } } } } // MARK: - Action Button struct ActionButton: View { let title: String let icon: String? let style: ButtonStyle let action: () -> Void enum ButtonStyle { case primary // Gold gradient (Deal, New Round) case destructive // Red (Clear) case secondary // Subtle white case custom(Color) // Game-specific colors (Hit, Stand, etc.) var foregroundColor: Color { switch self { case .primary: return .black case .destructive, .secondary, .custom: return .white } } } init(_ title: String, icon: String? = nil, style: ButtonStyle = .primary, action: @escaping () -> Void) { self.title = title self.icon = icon self.style = style self.action = action } var body: some View { Button(action: action) { HStack(spacing: Design.Spacing.small) { if let icon = icon { Image(systemName: icon) } Text(title) } .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) .foregroundStyle(style.foregroundColor) .padding(.horizontal, Design.Spacing.xxLarge) .padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall) .background(backgroundView) } .accessibilityLabel(title) } @ViewBuilder private var backgroundView: some View { switch style { case .primary: Capsule() .fill( LinearGradient( colors: [Color.Button.goldLight, Color.Button.goldDark], startPoint: .top, endPoint: .bottom ) ) case .destructive: Capsule() .fill(Color.red.opacity(Design.Opacity.heavy)) case .secondary: Capsule() .fill(Color.white.opacity(Design.Opacity.hint)) case .custom(let color): Capsule() .fill(color) } } } // MARK: - Preview #Preview { GameTableView() }