CasinoGames/Baccarat/Views/MiniBaccaratTableView.swift

476 lines
15 KiB
Swift

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