CasinoGames/Baccarat/Views/MiniBaccaratTableView.swift

554 lines
18 KiB
Swift

//
// 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()
}
}