// // MiniBaccaratTableView.swift // Baccarat // // A modern baccarat table layout with all betting options. // import SwiftUI import CasinoKit /// The baccarat betting table layout with main bets and side bets. struct MiniBaccaratTableView: View { @Bindable var gameState: GameState let selectedChip: ChipDenomination // MARK: - Fixed Font Sizes private let tableLimitsFontSize: CGFloat = Design.BaseFontSize.small // MARK: - Computed Properties 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 } private var tableLimitsText: String { String.localized( "tableLimitsFormat", gameState.minBet.formatted(), gameState.maxBet.formatted() ) } // MARK: - Body var body: some View { VStack(spacing: Design.Spacing.small) { // Table limits label Text(tableLimitsText) .font(.system(size: tableLimitsFontSize, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) .tracking(1) .lineLimit(1) .minimumScaleFactor(Design.MinScaleFactor.comfortable) // Main betting table VStack(spacing: 0) { // Top row: B PAIR | TIE | P PAIR TopBettingRow( bankerPairAmount: betAmount(for: .bankerPair), tieAmount: betAmount(for: .tie), playerPairAmount: betAmount(for: .playerPair), canBetBankerPair: canAddBet(for: .bankerPair), canBetTie: canAddBet(for: .tie), canBetPlayerPair: canAddBet(for: .playerPair), isBankerPairAtMax: isAtMax(for: .bankerPair), isTieAtMax: isAtMax(for: .tie), isPlayerPairAtMax: isAtMax(for: .playerPair), onBankerPair: { gameState.placeBet(type: .bankerPair, amount: selectedChip.rawValue) }, onTie: { gameState.placeBet(type: .tie, amount: selectedChip.rawValue) }, onPlayerPair: { gameState.placeBet(type: .playerPair, amount: selectedChip.rawValue) } ) // Divider Rectangle() .fill(Color.Border.gold.opacity(0.5)) .frame(height: 1) // Middle row: BANKER | BONUS MainBetRow( title: "BANKER", payoutText: "0.95 : 1", mainBetAmount: betAmount(for: .banker), bonusBetAmount: betAmount(for: .dragonBonusBanker), isSelected: isBankerSelected, canBetMain: canAddBet(for: .banker), canBetBonus: canAddBet(for: .dragonBonusBanker), isMainAtMax: isAtMax(for: .banker), isBonusAtMax: isAtMax(for: .dragonBonusBanker), mainColor: Color.BettingZone.bankerDark, onMain: { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) }, onBonus: { gameState.placeBet(type: .dragonBonusBanker, amount: selectedChip.rawValue) } ) // Divider Rectangle() .fill(Color.Border.gold.opacity(0.5)) .frame(height: 1) // Bottom row: PLAYER | BONUS MainBetRow( title: "PLAYER", payoutText: "1 : 1", mainBetAmount: betAmount(for: .player), bonusBetAmount: betAmount(for: .dragonBonusPlayer), isSelected: isPlayerSelected, canBetMain: canAddBet(for: .player), canBetBonus: canAddBet(for: .dragonBonusPlayer), isMainAtMax: isAtMax(for: .player), isBonusAtMax: isAtMax(for: .dragonBonusPlayer), mainColor: Color.BettingZone.playerDark, onMain: { gameState.placeBet(type: .player, amount: selectedChip.rawValue) }, onBonus: { gameState.placeBet(type: .dragonBonusPlayer, amount: selectedChip.rawValue) } ) } .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large)) .overlay( RoundedRectangle(cornerRadius: Design.CornerRadius.large) .strokeBorder( LinearGradient( colors: [Color.Border.goldLight, Color.Border.goldDark], startPoint: .top, endPoint: .bottom ), lineWidth: Design.LineWidth.medium ) ) .shadow(color: .black.opacity(0.3), radius: 10, y: 5) } } } // MARK: - Top Betting Row (B PAIR | TIE | P PAIR) private struct TopBettingRow: View { let bankerPairAmount: Int let tieAmount: Int let playerPairAmount: Int let canBetBankerPair: Bool let canBetTie: Bool let canBetPlayerPair: Bool let isBankerPairAtMax: Bool let isTieAtMax: Bool let isPlayerPairAtMax: Bool let onBankerPair: () -> Void let onTie: () -> Void let onPlayerPair: () -> Void private let rowHeight: CGFloat = 52 var body: some View { HStack(spacing: 0) { // B PAIR PairBetZone( title: "B PAIR", betAmount: bankerPairAmount, isEnabled: canBetBankerPair, isAtMax: isBankerPairAtMax, color: Color.BettingZone.bankerDark.opacity(0.6), action: onBankerPair ) // Vertical divider Rectangle() .fill(Color.Border.gold.opacity(0.5)) .frame(width: 1) // TIE TieBetZone( betAmount: tieAmount, isEnabled: canBetTie, isAtMax: isTieAtMax, action: onTie ) // Vertical divider Rectangle() .fill(Color.Border.gold.opacity(0.5)) .frame(width: 1) // P PAIR PairBetZone( title: "P PAIR", betAmount: playerPairAmount, isEnabled: canBetPlayerPair, isAtMax: isPlayerPairAtMax, color: Color.BettingZone.playerDark.opacity(0.6), action: onPlayerPair ) } .frame(height: rowHeight) } } // MARK: - Pair Bet Zone private struct PairBetZone: View { let title: String let betAmount: Int let isEnabled: Bool let isAtMax: Bool let color: Color let action: () -> Void private let titleFontSize: CGFloat = 12 private let payoutFontSize: CGFloat = 10 var body: some View { Button { if isEnabled { action() } } label: { ZStack { // Background Rectangle() .fill(color) // Content VStack(spacing: 2) { Text(title) .font(.system(size: titleFontSize, weight: .heavy, design: .rounded)) .foregroundStyle(.yellow) Text("11 : 1") .font(.system(size: payoutFontSize, weight: .medium, design: .rounded)) .foregroundStyle(.white.opacity(0.7)) } // Chip indicator if betAmount > 0 { ChipBadge(amount: betAmount, isMax: isAtMax) .offset(y: 16) } } } .buttonStyle(.plain) .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) .accessibilityLabel("\(title) bet, pays 11 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) } } // MARK: - Tie Bet Zone private struct TieBetZone: View { let betAmount: Int let isEnabled: Bool let isAtMax: Bool let action: () -> Void private let titleFontSize: CGFloat = 14 private let payoutFontSize: CGFloat = 10 var body: some View { Button { if isEnabled { action() } } label: { ZStack { // Background Rectangle() .fill(isAtMax ? Color.BettingZone.tieMax : Color.BettingZone.tie) // Content VStack(spacing: 2) { Text("TIE") .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .tracking(1) Text("8 : 1") .font(.system(size: payoutFontSize, weight: .medium, design: .rounded)) .opacity(0.7) } .foregroundStyle(.white) // Chip indicator if betAmount > 0 { ChipBadge(amount: betAmount, isMax: isAtMax) .offset(y: 16) } } } .buttonStyle(.plain) .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) .accessibilityLabel("Tie bet, pays 8 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) } } // MARK: - Main Bet Row (BANKER/PLAYER with BONUS) private struct MainBetRow: View { let title: String let payoutText: String let mainBetAmount: Int let bonusBetAmount: Int let isSelected: Bool let canBetMain: Bool let canBetBonus: Bool let isMainAtMax: Bool let isBonusAtMax: Bool let mainColor: Color let onMain: () -> Void let onBonus: () -> Void private let rowHeight: CGFloat = 65 private let bonusWidth: CGFloat = 80 var body: some View { HStack(spacing: 0) { // Main bet zone (BANKER or PLAYER) MainBetZone( title: title, payoutText: payoutText, betAmount: mainBetAmount, isSelected: isSelected, isEnabled: canBetMain, isAtMax: isMainAtMax, color: mainColor, action: onMain ) // Vertical divider Rectangle() .fill(Color.Border.gold.opacity(0.5)) .frame(width: 1) // Dragon Bonus zone DragonBonusZone( betAmount: bonusBetAmount, isEnabled: canBetBonus, isAtMax: isBonusAtMax, action: onBonus ) .frame(width: bonusWidth) } .frame(height: rowHeight) } } // MARK: - Main Bet Zone (BANKER or PLAYER) private struct MainBetZone: View { let title: String let payoutText: String let betAmount: Int let isSelected: Bool let isEnabled: Bool let isAtMax: Bool let color: Color let action: () -> Void private let titleFontSize: CGFloat = 20 private let payoutFontSize: CGFloat = 11 var body: some View { Button { if isEnabled { action() } } label: { ZStack { // Background Rectangle() .fill(color) // Selection highlight if isSelected { Rectangle() .fill(Color.yellow.opacity(0.15)) Rectangle() .strokeBorder(Color.yellow, lineWidth: 3) } // Content HStack { Spacer() VStack(spacing: 3) { Text(title) .font(.system(size: titleFontSize, weight: .black, design: .rounded)) .tracking(2) Text(payoutText) .font(.system(size: payoutFontSize, weight: .semibold, design: .rounded)) .opacity(0.7) } .foregroundStyle(.white) Spacer() // Chip indicator if betAmount > 0 { ChipOnTableView(amount: betAmount, showMax: isAtMax) .padding(.trailing, Design.Spacing.medium) } } } } .buttonStyle(.plain) .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) .accessibilityLabel("\(title) bet, pays \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) } } // MARK: - Dragon Bonus Zone private struct DragonBonusZone: View { let betAmount: Int let isEnabled: Bool let isAtMax: Bool let action: () -> Void private let titleFontSize: CGFloat = 9 private let diamondSize: CGFloat = 24 var body: some View { Button { if isEnabled { action() } } label: { ZStack { // Background - darker purple/blue tint Rectangle() .fill( LinearGradient( colors: [ Color(red: 0.25, green: 0.3, blue: 0.45), Color(red: 0.15, green: 0.2, blue: 0.35) ], startPoint: .top, endPoint: .bottom ) ) // Content VStack(spacing: 5) { // Diamond shape DiamondShape() .fill( LinearGradient( colors: [Color.purple.opacity(0.8), Color.purple.opacity(0.5)], startPoint: .top, endPoint: .bottom ) ) .frame(width: diamondSize, height: diamondSize) .overlay( DiamondShape() .strokeBorder(Color.white.opacity(0.4), lineWidth: 1) ) Text("BONUS") .font(.system(size: titleFontSize, weight: .bold, design: .rounded)) .foregroundStyle(.white.opacity(0.9)) } // Chip indicator if betAmount > 0 { ChipBadge(amount: betAmount, isMax: isAtMax) .offset(y: 22) } } } .buttonStyle(.plain) .opacity(isEnabled ? 1.0 : 0.5) .accessibilityElement(children: .ignore) .accessibilityLabel("Dragon Bonus, pays up to 30 to 1" + (betAmount > 0 ? ", current bet $\(betAmount)" : "")) .accessibilityAddTraits(.isButton) } } // MARK: - Diamond Shape private struct DiamondShape: InsettableShape { var insetAmount: CGFloat = 0 func path(in rect: CGRect) -> Path { var path = Path() let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount) path.move(to: CGPoint(x: insetRect.midX, y: insetRect.minY)) path.addLine(to: CGPoint(x: insetRect.maxX, y: insetRect.midY)) path.addLine(to: CGPoint(x: insetRect.midX, y: insetRect.maxY)) path.addLine(to: CGPoint(x: insetRect.minX, y: insetRect.midY)) path.closeSubpath() return path } func inset(by amount: CGFloat) -> some InsettableShape { var shape = self shape.insetAmount += amount return shape } } // MARK: - Chip Badge (small indicator) private struct ChipBadge: View { let amount: Int let isMax: Bool var body: some View { ZStack { Circle() .fill(isMax ? Color.gray : Color.yellow) .frame(width: 20, height: 20) Circle() .strokeBorder(Color.white.opacity(0.8), lineWidth: 1) .frame(width: 20, height: 20) if isMax { Text("M") .font(.system(size: 9, weight: .bold)) .foregroundStyle(.white) } else { Text(formatCompact(amount)) .font(.system(size: 7, weight: .bold)) .foregroundStyle(.black) } } } private func formatCompact(_ value: Int) -> String { if value >= 1000 { return "\(value / 1000)K" } return "\(value)" } } #Preview { ZStack { Color.Table.baseDark .ignoresSafeArea() MiniBaccaratTableView( gameState: GameState(), selectedChip: .hundred ) .padding() } }