// // 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( feltColor: Color.Table.felt, edgeColor: Color.Table.feltDark ) VStack(spacing: 0) { // Top bar TopBarView( balance: state.balance, secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : 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, 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 blackjack if state.showResultBanner && (state.lastRoundResult?.wasBlackjack ?? false) { 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.currentBet >= state.settings.minBet { 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 case destructive case secondary case custom(Color) var foregroundColor: Color { switch self { case .primary: return .black case .destructive, .secondary, .custom: return .white } } var backgroundColor: Color { switch self { case .primary: return .yellow case .destructive: return .red.opacity(Design.Opacity.heavy) case .secondary: return .white.opacity(Design.Opacity.hint) case .custom(let color): return color } } } 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.medium, weight: .bold)) .foregroundStyle(style.foregroundColor) .padding(.horizontal, Design.Spacing.xLarge) .padding(.vertical, Design.Spacing.medium) .background( Capsule() .fill(style.backgroundColor) ) .shadow(color: style.backgroundColor.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) } .accessibilityLabel(title) } } // MARK: - Preview #Preview { GameTableView() }