// // MiniBaccaratTableView.swift // Baccarat // // A realistic mini baccarat table layout for single player. // import SwiftUI /// The semi-circular mini baccarat betting table layout. struct MiniBaccaratTableView: View { @Bindable var gameState: GameState let selectedChip: ChipDenomination private func betAmount(for type: BetType) -> Int { gameState.betAmount(for: type) } private func canAddBet(for type: BetType) -> Bool { gameState.canPlaceBet && gameState.balance >= selectedChip.rawValue && gameState.canAddToBet(type: type, amount: selectedChip.rawValue) } private func isAtMax(for type: BetType) -> Bool { betAmount(for: type) >= gameState.maxBet } private var isPlayerSelected: Bool { gameState.mainBet?.type == .player } private var isBankerSelected: Bool { gameState.mainBet?.type == .banker } var body: some View { VStack(spacing: 4) { // Table limits label Text("TABLE LIMITS: $\(gameState.minBet) - $\(gameState.maxBet.formatted())") .font(.system(size: 10, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.5)) .tracking(1) ZStack { // Table felt background with arc shape TableFeltShape() .fill( LinearGradient( colors: [ Color(red: 0.0, green: 0.35, blue: 0.18), Color(red: 0.0, green: 0.28, blue: 0.12) ], startPoint: .top, endPoint: .bottom ) ) // Gold border TableFeltShape() .strokeBorder( LinearGradient( colors: [ Color(red: 0.85, green: 0.7, blue: 0.35), Color(red: 0.65, green: 0.5, blue: 0.2) ], startPoint: .top, endPoint: .bottom ), lineWidth: 4 ) // Betting zones layout VStack(spacing: 0) { // TIE zone at top TieBettingZone( betAmount: betAmount(for: .tie), isEnabled: canAddBet(for: .tie), isAtMax: isAtMax(for: .tie) ) { gameState.placeBet(type: .tie, amount: selectedChip.rawValue) } .frame(height: 55) .padding(.horizontal, 50) .padding(.top, 12) Spacer(minLength: 8) // BANKER zone in middle BankerBettingZone( betAmount: betAmount(for: .banker), isSelected: isBankerSelected, isEnabled: canAddBet(for: .banker), isAtMax: isAtMax(for: .banker) ) { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) } .frame(height: 60) .padding(.horizontal, 30) Spacer(minLength: 8) // PLAYER zone at bottom PlayerBettingZone( betAmount: betAmount(for: .player), isSelected: isPlayerSelected, isEnabled: canAddBet(for: .player), isAtMax: isAtMax(for: .player) ) { gameState.placeBet(type: .player, amount: selectedChip.rawValue) } .frame(height: 60) .padding(.horizontal, 20) .padding(.bottom, 12) } } .aspectRatio(1.6, contentMode: .fit) } } } /// Custom shape for the mini baccarat table felt. struct TableFeltShape: InsettableShape { var insetAmount: CGFloat = 0 func path(in rect: CGRect) -> Path { var path = Path() let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount) let height = insetRect.height let cornerRadius: CGFloat = 20 // Start from bottom left path.move(to: CGPoint(x: insetRect.minX + cornerRadius, y: insetRect.maxY)) // Bottom edge path.addLine(to: CGPoint(x: insetRect.maxX - cornerRadius, y: insetRect.maxY)) // Bottom right corner path.addQuadCurve( to: CGPoint(x: insetRect.maxX, y: insetRect.maxY - cornerRadius), control: CGPoint(x: insetRect.maxX, y: insetRect.maxY) ) // Right edge going up with curve path.addLine(to: CGPoint(x: insetRect.maxX, y: insetRect.minY + height * 0.3)) // Top arc path.addQuadCurve( to: CGPoint(x: insetRect.minX, y: insetRect.minY + height * 0.3), control: CGPoint(x: insetRect.midX, y: insetRect.minY - height * 0.1) ) // Left edge going down path.addLine(to: CGPoint(x: insetRect.minX, y: insetRect.maxY - cornerRadius)) // Bottom left corner path.addQuadCurve( to: CGPoint(x: insetRect.minX + cornerRadius, y: insetRect.maxY), control: CGPoint(x: insetRect.minX, y: insetRect.maxY) ) path.closeSubpath() return path } func inset(by amount: CGFloat) -> some InsettableShape { var shape = self shape.insetAmount += amount return shape } } /// The TIE betting zone at the top of the table. struct TieBettingZone: View { let betAmount: Int let isEnabled: Bool var isAtMax: Bool = false let action: () -> Void private var backgroundColor: Color { if isAtMax { // Darker/muted green when at max return Color(red: 0.08, green: 0.32, blue: 0.18) } return Color(red: 0.1, green: 0.45, blue: 0.25) } private var borderColor: Color { if isAtMax { // Silver border when at max return Color(red: 0.6, green: 0.6, blue: 0.65) } return Color(red: 0.7, green: 0.55, blue: 0.25) } var body: some View { Button { if isEnabled { action() } } label: { ZStack { // Background RoundedRectangle(cornerRadius: 8) .fill(backgroundColor) // Border RoundedRectangle(cornerRadius: 8) .strokeBorder(borderColor, lineWidth: 2) // Centered text content VStack(spacing: 2) { Text("TIE") .font(.system(size: 14, weight: .black, design: .rounded)) .tracking(2) Text("PAYS 8 TO 1") .font(.system(size: 9, weight: .medium)) .opacity(0.8) } .foregroundStyle(.white) } // Chip overlaid on right side .overlay(alignment: .trailing) { if betAmount > 0 { ChipOnTable(amount: betAmount, showMax: isAtMax) .padding(.trailing, 8) } } } .buttonStyle(.plain) } } /// The BANKER betting zone in the middle of the table. struct BankerBettingZone: View { let betAmount: Int let isSelected: Bool let isEnabled: Bool var isAtMax: Bool = false let action: () -> Void private var backgroundColors: [Color] { if isAtMax { // Darker/muted red when at max return [ Color(red: 0.4, green: 0.1, blue: 0.1), Color(red: 0.28, green: 0.06, blue: 0.06) ] } return [ Color(red: 0.55, green: 0.12, blue: 0.12), Color(red: 0.4, green: 0.08, blue: 0.08) ] } private var borderColor: Color { if isAtMax { return Color(red: 0.6, green: 0.6, blue: 0.65) } return Color(red: 0.7, green: 0.55, blue: 0.25) } var body: some View { Button { if isEnabled { action() } } label: { ZStack { // Background RoundedRectangle(cornerRadius: 10) .fill( LinearGradient( colors: backgroundColors, startPoint: .top, endPoint: .bottom ) ) // Selection glow if isSelected { RoundedRectangle(cornerRadius: 10) .strokeBorder(Color.yellow, lineWidth: 3) .shadow(color: .yellow.opacity(0.5), radius: 8) } // Border RoundedRectangle(cornerRadius: 10) .strokeBorder(borderColor, lineWidth: 2) // Centered text content VStack(spacing: 2) { Text("BANKER") .font(.system(size: 16, weight: .black, design: .rounded)) .tracking(3) Text("PAYS 0.95 TO 1") .font(.system(size: 9, weight: .medium)) .opacity(0.8) } .foregroundStyle(.white) } // Chip overlaid on right side .overlay(alignment: .trailing) { if betAmount > 0 { ChipOnTable(amount: betAmount, showMax: isAtMax) .padding(.trailing, 12) } } } .buttonStyle(.plain) } } /// The PLAYER betting zone at the bottom of the table. struct PlayerBettingZone: View { let betAmount: Int let isSelected: Bool let isEnabled: Bool var isAtMax: Bool = false let action: () -> Void private var backgroundColors: [Color] { if isAtMax { // Darker/muted blue when at max return [ Color(red: 0.08, green: 0.18, blue: 0.4), Color(red: 0.04, green: 0.1, blue: 0.28) ] } return [ Color(red: 0.1, green: 0.25, blue: 0.55), Color(red: 0.05, green: 0.15, blue: 0.4) ] } private var borderColor: Color { if isAtMax { return Color(red: 0.6, green: 0.6, blue: 0.65) } return Color(red: 0.7, green: 0.55, blue: 0.25) } var body: some View { Button { if isEnabled { action() } } label: { ZStack { // Background RoundedRectangle(cornerRadius: 10) .fill( LinearGradient( colors: backgroundColors, startPoint: .top, endPoint: .bottom ) ) // Selection glow if isSelected { RoundedRectangle(cornerRadius: 10) .strokeBorder(Color.yellow, lineWidth: 3) .shadow(color: .yellow.opacity(0.5), radius: 8) } // Border RoundedRectangle(cornerRadius: 10) .strokeBorder(borderColor, lineWidth: 2) // Centered text content VStack(spacing: 2) { Text("PLAYER") .font(.system(size: 16, weight: .black, design: .rounded)) .tracking(3) Text("PAYS 1 TO 1") .font(.system(size: 9, weight: .medium)) .opacity(0.8) } .foregroundStyle(.white) } // Chip overlaid on right side .overlay(alignment: .trailing) { if betAmount > 0 { ChipOnTable(amount: betAmount, showMax: isAtMax) .padding(.trailing, 12) } } } .buttonStyle(.plain) } } /// A chip displayed on the table showing bet amount. struct ChipOnTable: View { let amount: Int var showMax: Bool = false private var chipColor: Color { switch amount { case 0..<50: return .blue case 50..<100: return .orange case 100..<500: return .black case 500..<1000: return .purple default: return Color(red: 0.8, green: 0.65, blue: 0.2) } } private var displayText: String { if amount >= 1000 { return "\(amount / 1000)K" } else { return "\(amount)" } } var body: some View { ZStack { Circle() .fill( RadialGradient( colors: [chipColor.opacity(0.9), chipColor], center: .topLeading, startRadius: 0, endRadius: 20 ) ) .frame(width: 36, height: 36) Circle() .strokeBorder(Color.white.opacity(0.8), lineWidth: 2) .frame(width: 36, height: 36) Circle() .strokeBorder(Color.white.opacity(0.4), lineWidth: 1) .frame(width: 26, height: 26) Text(displayText) .font(.system(size: amount >= 1000 ? 10 : 11, weight: .bold)) .foregroundStyle(.white) } .shadow(color: .black.opacity(0.4), radius: 3, x: 1, y: 2) .overlay(alignment: .topTrailing) { if showMax { Text("MAX") .font(.system(size: 7, weight: .black)) .foregroundStyle(.white) .padding(.horizontal, 4) .padding(.vertical, 2) .background( Capsule() .fill(Color.red) ) .offset(x: 6, y: -4) } } } } #Preview { ZStack { Color(red: 0.05, green: 0.2, blue: 0.1) .ignoresSafeArea() MiniBaccaratTableView( gameState: GameState(), selectedChip: .hundred ) .padding() } }