// // GameTableView.swift // Baccarat // // The main baccarat table view with all game elements. // import SwiftUI /// The main game table view containing all game elements. struct GameTableView: View { @State private var settings = GameSettings() @State private var gameState: GameState? @State private var selectedChip: ChipDenomination = .hundred @State private var showSettings = false private var state: GameState { gameState ?? GameState(settings: settings) } private var playerIsWinner: Bool { state.lastResult == .playerWins } private var bankerIsWinner: Bool { state.lastResult == .bankerWins } private var isTie: Bool { state.lastResult == .tie } var body: some View { ZStack { // Table background TableBackgroundView() // Main content VStack(spacing: 0) { // Top bar with balance and info TopBarView( balance: state.balance, cardsRemaining: state.engine.shoe.cardsRemaining, showCardsRemaining: settings.showCardsRemaining, onReset: { state.resetGame() }, onSettings: { showSettings = true } ) Spacer(minLength: 4) // Cards display area CardsDisplayArea( playerCards: state.visiblePlayerCards, bankerCards: state.visibleBankerCards, playerCardsFaceUp: state.playerCardsFaceUp, bankerCardsFaceUp: state.bankerCardsFaceUp, playerValue: state.playerHandValue, bankerValue: state.bankerHandValue, playerIsWinner: playerIsWinner, bankerIsWinner: bankerIsWinner, isTie: isTie ) Spacer(minLength: 4) // Road map history if settings.showHistory && !state.roundHistory.isEmpty { RoadMapView(results: state.recentResults) .padding(.horizontal) } Spacer(minLength: 8) // Mini Baccarat betting table MiniBaccaratTableView( gameState: state, selectedChip: selectedChip ) .padding(.horizontal, 12) Spacer(minLength: 8) // Chip selector - shows higher chips as you win more! ChipSelectorView( selectedChip: $selectedChip, balance: state.balance, maxBet: state.maxBet ) .padding(.bottom, 12) // Action buttons ActionButtonsView( gameState: state, onDeal: { Task { await state.deal() } }, onClear: { state.clearBets() }, onNewRound: { state.newRound() } ) .padding(.horizontal) .padding(.bottom, 4) } .safeAreaPadding(.bottom) // Result banner overlay if state.showResultBanner, let result = state.lastResult { ResultBannerView( result: result, winnings: state.lastWinnings ) .transition(.opacity) // Confetti for wins if state.lastWinnings > 0 { ConfettiView() } } // Game Over overlay when broke if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating { GameOverView( roundsPlayed: state.roundHistory.count, onPlayAgain: { state.resetGame() } ) .transition(.opacity) } } .onAppear { if gameState == nil { gameState = GameState(settings: settings) } } .sheet(isPresented: $showSettings) { SettingsView(settings: settings) { // Apply settings when changed gameState?.applySettings() } } } } /// Game over screen shown when player runs out of money. struct GameOverView: View { let roundsPlayed: Int let onPlayAgain: () -> Void @State private var showContent = false // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .largeTitle) private var iconSize: CGFloat = Design.BaseFontSize.display @ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = Design.BaseFontSize.largeTitle @ScaledMetric(relativeTo: .body) private var messageFontSize: CGFloat = Design.BaseFontSize.xLarge @ScaledMetric(relativeTo: .body) private var statsFontSize: CGFloat = 17 @ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.xLarge // MARK: - Layout Constants private let modalCornerRadius = Design.CornerRadius.xxxLarge private let statsCornerRadius = Design.CornerRadius.large private let cardPadding = Design.Spacing.xxxLarge private let contentSpacing: CGFloat = 28 private let buttonHorizontalPadding: CGFloat = 48 private let buttonVerticalPadding: CGFloat = 18 // MARK: - Body var body: some View { ZStack { // Solid dark backdrop - fully opaque Color.black .ignoresSafeArea() // Modal card VStack(spacing: contentSpacing) { // Broke icon Image(systemName: "creditcard.trianglebadge.exclamationmark") .font(.system(size: iconSize)) .foregroundStyle(.red) .symbolEffect(.pulse, options: .repeating) // Title Text("GAME OVER") .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .foregroundStyle(.white) // Message Text("You've run out of chips!") .font(.system(size: messageFontSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.strong)) // Stats card VStack(spacing: Design.Spacing.medium) { HStack { Text("Rounds Played") .foregroundStyle(.white.opacity(Design.Opacity.medium)) Spacer() Text("\(roundsPlayed)") .bold() .foregroundStyle(.white) } } .font(.system(size: statsFontSize)) .padding() .background( RoundedRectangle(cornerRadius: statsCornerRadius) .fill(Color.white.opacity(0.08)) .overlay( RoundedRectangle(cornerRadius: statsCornerRadius) .strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin) ) ) .padding(.horizontal, Design.Spacing.xLarge) // Play Again button Button { onPlayAgain() } label: { HStack(spacing: Design.Spacing.small) { Image(systemName: "arrow.counterclockwise") Text("Play Again") } .font(.system(size: buttonFontSize, weight: .bold)) .foregroundStyle(.black) .padding(.horizontal, buttonHorizontalPadding) .padding(.vertical, buttonVerticalPadding) .background( Capsule() .fill( LinearGradient( colors: [Color.Button.goldLight, Color.Button.goldDark], startPoint: .top, endPoint: .bottom ) ) ) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusXLarge) } .padding(.top, Design.Spacing.medium) } .padding(cardPadding) .background( RoundedRectangle(cornerRadius: modalCornerRadius) .fill( LinearGradient( colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark], startPoint: .top, endPoint: .bottom ) ) .overlay( RoundedRectangle(cornerRadius: modalCornerRadius) .strokeBorder( LinearGradient( colors: [ Color.red.opacity(Design.Opacity.medium), Color.red.opacity(0.2) ], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: Design.LineWidth.medium ) ) ) .shadow(color: .red.opacity(0.2), radius: Design.Shadow.radiusXXLarge) .padding(.horizontal, Design.Spacing.xxLarge) .scaleEffect(showContent ? 1.0 : 0.8) .opacity(showContent ? 1.0 : 0) } .onAppear { withAnimation(.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)) { showContent = true } } } } /// The cards display area showing both hands. struct CardsDisplayArea: View { let playerCards: [Card] let bankerCards: [Card] let playerCardsFaceUp: [Bool] let bankerCardsFaceUp: [Bool] let playerValue: Int let bankerValue: Int let playerIsWinner: Bool let bankerIsWinner: Bool let isTie: Bool // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = 14 var body: some View { HStack(spacing: 32) { // Player side VStack(spacing: 10) { // Label with value HStack(spacing: 8) { Text("PLAYER") .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) .foregroundStyle(.white) if !playerCards.isEmpty && playerCardsFaceUp.contains(true) { ValueBadge(value: playerValue, color: .blue) } } .frame(minHeight: 30) // Cards CompactHandView( cards: playerCards, cardsFaceUp: playerCardsFaceUp, isWinner: playerIsWinner ) } // Banker side VStack(spacing: 10) { // Label with value HStack(spacing: 8) { Text("BANKER") .font(.system(size: labelFontSize, weight: .bold, design: .rounded)) .foregroundStyle(.white) if !bankerCards.isEmpty && bankerCardsFaceUp.contains(true) { ValueBadge(value: bankerValue, color: .red) } } .frame(minHeight: 30) // Cards CompactHandView( cards: bankerCards, cardsFaceUp: bankerCardsFaceUp, isWinner: bankerIsWinner ) } } .padding(.top, 16) .padding(.bottom, 14) .padding(.horizontal, 20) .background( RoundedRectangle(cornerRadius: 14) .fill(Color.black.opacity(0.25)) ) .padding(.horizontal) } } /// A compact hand view showing cards in a row. struct CompactHandView: View { let cards: [Card] let cardsFaceUp: [Bool] let isWinner: Bool // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .caption) private var winBadgeFontSize: CGFloat = 10 // MARK: - Layout Constants // Fixed size: cards have strict visual constraints private let cardWidth: CGFloat = 45 var body: some View { HStack(spacing: -12) { if cards.isEmpty { // Placeholders ForEach(0..<2, id: \.self) { _ in CardPlaceholderView(width: cardWidth) } } else { ForEach(cards.indices, id: \.self) { index in let isFaceUp = index < cardsFaceUp.count ? cardsFaceUp[index] : false CardView( card: cards[index], isFaceUp: isFaceUp, cardWidth: cardWidth ) .zIndex(Double(index)) } } } .padding(6) .background( RoundedRectangle(cornerRadius: 8) .strokeBorder( isWinner ? Color.yellow : Color.clear, lineWidth: 2 ) ) .overlay(alignment: .bottom) { if isWinner { Text("WIN") .font(.system(size: winBadgeFontSize, weight: .black)) .foregroundStyle(.black) .padding(.horizontal, 8) .padding(.vertical, 2) .background( Capsule() .fill(Color.yellow) ) .offset(y: 10) } } } } /// A small badge showing the hand value. struct ValueBadge: View { let value: Int let color: Color // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .headline) private var valueFontSize: CGFloat = 15 @ScaledMetric(relativeTo: .headline) private var badgeSize: CGFloat = 26 var body: some View { Text("\(value)") .font(.system(size: valueFontSize, weight: .black, design: .rounded)) .foregroundStyle(.white) .frame(width: badgeSize, height: badgeSize) .background( Circle() .fill(color) ) } } /// The casino table background. struct TableBackgroundView: View { var body: some View { ZStack { // Base dark green Color(red: 0.02, green: 0.15, blue: 0.08) // Radial gradient for depth RadialGradient( colors: [ Color(red: 0.03, green: 0.25, blue: 0.12), Color(red: 0.01, green: 0.12, blue: 0.06) ], center: .center, startRadius: 50, endRadius: 500 ) // Subtle felt texture FeltPatternView() .opacity(0.03) } .ignoresSafeArea() } } /// Subtle felt texture pattern. struct FeltPatternView: View { var body: some View { Canvas { context, size in for _ in 0..<2000 { let x = Double.random(in: 0...size.width) let y = Double.random(in: 0...size.height) let dotSize = Double.random(in: 1...2) let rect = CGRect(x: x, y: y, width: dotSize, height: dotSize) context.fill(Path(ellipseIn: rect), with: .color(.white)) } } } } /// Top bar showing balance and game info. struct TopBarView: View { let balance: Int let cardsRemaining: Int let showCardsRemaining: Bool let onReset: () -> Void let onSettings: () -> Void // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .caption2) private var labelFontSize: CGFloat = 9 @ScaledMetric(relativeTo: .body) private var currencyFontSize: CGFloat = 14 @ScaledMetric(relativeTo: .title3) private var balanceFontSize: CGFloat = 20 @ScaledMetric(relativeTo: .caption) private var smallFontSize: CGFloat = 12 @ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = 16 var body: some View { HStack { // Balance display VStack(alignment: .leading, spacing: 2) { Text("BALANCE") .font(.system(size: labelFontSize, weight: .medium, design: .rounded)) .foregroundStyle(.white.opacity(0.6)) .tracking(1) HStack(spacing: 4) { Text("$") .font(.system(size: currencyFontSize, weight: .bold)) .foregroundStyle(.yellow.opacity(0.8)) Text(balance, format: .number) .font(.system(size: balanceFontSize, weight: .black, design: .rounded)) .foregroundStyle(.white) .contentTransition(.numericText()) .animation(.spring(duration: 0.3), value: balance) } } .padding(.horizontal, 14) .padding(.vertical, 6) .background( Capsule() .fill(Color.black.opacity(0.4)) ) Spacer() // Cards remaining indicator (if enabled) if showCardsRemaining { HStack(spacing: 4) { Image(systemName: "rectangle.portrait.on.rectangle.portrait.fill") .font(.system(size: smallFontSize)) Text("\(cardsRemaining)") .font(.system(size: smallFontSize, weight: .medium)) } .foregroundStyle(.white.opacity(0.5)) Spacer() } // Settings button Button("Settings", systemImage: "gearshape.fill", action: onSettings) .labelStyle(.iconOnly) .font(.system(size: buttonFontSize)) .foregroundStyle(.white.opacity(0.6)) .padding(8) .background( Circle() .fill(Color.black.opacity(0.4)) ) // Reset button Button("Reset", systemImage: "arrow.counterclockwise", action: onReset) .font(.system(size: smallFontSize, weight: .medium)) .foregroundStyle(.white.opacity(0.6)) .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() .fill(Color.black.opacity(0.4)) ) } .padding(.horizontal) .padding(.top, 4) } } /// Action buttons for deal, clear, and new round. struct ActionButtonsView: View { @Bindable var gameState: GameState let onDeal: () -> Void let onClear: () -> Void let onNewRound: () -> Void // MARK: - Scaled Font Sizes (Dynamic Type) @ScaledMetric(relativeTo: .body) private var clearButtonFontSize: CGFloat = 14 @ScaledMetric(relativeTo: .headline) private var primaryButtonFontSize: CGFloat = 16 @ScaledMetric(relativeTo: .body) private var statusFontSize: CGFloat = 14 var body: some View { HStack(spacing: 12) { if gameState.currentPhase == .betting { // Clear bets button Button("Clear", systemImage: "xmark.circle", action: onClear) .font(.system(size: clearButtonFontSize, weight: .semibold)) .foregroundStyle(.white) .padding(.horizontal, 20) .padding(.vertical, 12) .background( Capsule() .fill(Color.Button.destructive) ) .opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0) .disabled(gameState.currentBets.isEmpty) // Deal button Button("Deal", systemImage: "play.fill", action: onDeal) .font(.system(size: primaryButtonFontSize, weight: .bold)) .foregroundStyle(.black) .padding(.horizontal, 32) .padding(.vertical, 12) .background( Capsule() .fill( LinearGradient( colors: [Color.Button.goldLight, Color.Button.goldDark], startPoint: .top, endPoint: .bottom ) ) ) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) .opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled) .disabled(!gameState.canDeal) } else if gameState.currentPhase == .roundComplete { // New round button Button("New Round", systemImage: "arrow.right.circle", action: onNewRound) .font(.system(size: primaryButtonFontSize, weight: .bold)) .foregroundStyle(.black) .padding(.horizontal, 32) .padding(.vertical, 12) .background( Capsule() .fill( LinearGradient( colors: [Color.Button.goldLight, Color.Button.goldDark], startPoint: .top, endPoint: .bottom ) ) ) .shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium) } else { // Playing indicator HStack(spacing: 6) { ProgressView() .tint(.white) .scaleEffect(0.8) Text("Dealing...") .font(.system(size: statusFontSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.heavy)) } .padding(.horizontal, 20) .padding(.vertical, 12) } } } } #Preview { GameTableView() }