CasinoGames/Blackjack/Blackjack/Views/Table/BettingZoneView.swift

194 lines
6.9 KiB
Swift

//
// BettingZoneView.swift
// Blackjack
//
// The betting area where players place their bets.
//
import SwiftUI
import CasinoKit
/// Main betting zone view - adapts layout based on whether side bets are enabled.
/// Follows Baccarat's betting pattern exactly.
struct BettingZoneView: View {
@Bindable var state: GameState
let selectedChip: ChipDenomination
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var detailFontSize: CGFloat = Design.Size.handNumberFontSize
@ScaledMetric(relativeTo: .body) private var chipSize: CGFloat = Design.Size.bettingChipSize
@ScaledMetric(relativeTo: .body) private var zoneHeight: CGFloat = CasinoDesign.Size.bettingZoneHeight
// MARK: - Computed Properties (matches Baccarat's pattern)
/// Whether a bet can be added to main bet (matches Baccarat's canAddBet)
private var canAddMainBet: Bool {
state.canPlaceBet &&
state.balance >= selectedChip.rawValue &&
state.canAddToMainBet(amount: selectedChip.rawValue)
}
/// Whether a bet can be added to Perfect Pairs
private var canAddPerfectPairs: Bool {
state.canPlaceBet &&
state.balance >= selectedChip.rawValue &&
state.canAddToSideBet(type: .perfectPairs, amount: selectedChip.rawValue)
}
/// Whether a bet can be added to 21+3
private var canAddTwentyOnePlusThree: Bool {
state.canPlaceBet &&
state.balance >= selectedChip.rawValue &&
state.canAddToSideBet(type: .twentyOnePlusThree, amount: selectedChip.rawValue)
}
private var isMainAtMax: Bool {
state.currentBet >= state.settings.maxBet
}
private var isPPAtMax: Bool {
state.perfectPairsBet >= state.settings.maxBet
}
private var is21PlusThreeAtMax: Bool {
state.twentyOnePlusThreeBet >= state.settings.maxBet
}
var body: some View {
if state.settings.sideBetsEnabled {
// Horizontal layout: PP | Main Bet | 21+3
HStack(spacing: Design.Spacing.small) {
// Perfect Pairs
SideBetZoneView(
betType: .perfectPairs,
betAmount: state.perfectPairsBet,
isEnabled: canAddPerfectPairs,
isAtMax: isPPAtMax,
onTap: { state.placeSideBet(type: .perfectPairs, amount: selectedChip.rawValue) }
)
.frame(width: Design.Size.sideBetZoneWidth)
// Main bet (center, takes remaining space)
mainBetZone
// 21+3
SideBetZoneView(
betType: .twentyOnePlusThree,
betAmount: state.twentyOnePlusThreeBet,
isEnabled: canAddTwentyOnePlusThree,
isAtMax: is21PlusThreeAtMax,
onTap: { state.placeSideBet(type: .twentyOnePlusThree, amount: selectedChip.rawValue) }
)
.frame(width: Design.Size.sideBetZoneWidth)
}
.frame(height: zoneHeight)
} else {
// Simple layout: just main bet
mainBetZone
.frame(height: zoneHeight)
}
}
private var mainBetZone: some View {
Button {
if canAddMainBet {
state.placeBet(amount: selectedChip.rawValue)
}
} label: {
ZStack {
// Background
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.BettingZone.main)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.strokeBorder(Color.BettingZone.mainBorder, lineWidth: Design.LineWidth.medium)
)
// Content
if state.currentBet > 0 {
// Show chip with amount (scaled)
ChipOnTableView(amount: state.currentBet, showMax: isMainAtMax, size: chipSize)
} else {
// Empty state
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "TAP TO BET"))
.font(.system(size: labelFontSize, weight: .bold))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
HStack(spacing: Design.Spacing.medium) {
Text(String(localized: "Min: $\(state.settings.minBet)"))
.font(.system(size: detailFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light))
Text(String(localized: "Max: $\(state.settings.maxBet.formatted())"))
.font(.system(size: detailFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light))
}
}
}
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.plain)
.accessibilityLabel(state.currentBet > 0 ? "$\(state.currentBet) bet" + (isMainAtMax ? ", maximum" : "") : "Place bet")
.accessibilityHint("Double tap to add chips")
}
}
// MARK: - Design Constants Extension
extension Design.Size {
/// Width of side bet zones
static let sideBetZoneWidth: CGFloat = 70
}
// MARK: - Previews
#Preview("Empty - No Side Bets") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingZoneView(
state: GameState(settings: GameSettings()),
selectedChip: .hundred
)
.padding()
}
}
#Preview("With Side Bets") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingZoneView(
state: {
let settings = GameSettings()
settings.sideBetsEnabled = true
let state = GameState(settings: settings)
state.placeBet(amount: 100)
return state
}(),
selectedChip: .twentyFive
)
.padding()
}
}
#Preview("All Bets Placed") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingZoneView(
state: {
let settings = GameSettings()
settings.sideBetsEnabled = true
let state = GameState(settings: settings)
state.placeBet(amount: 250)
state.placeSideBet(type: .perfectPairs, amount: 25)
state.placeSideBet(type: .twentyOnePlusThree, amount: 50)
return state
}(),
selectedChip: .hundred
)
.padding()
}
}