CasinoGames/Blackjack/Views/Table/BlackjackTableView.swift

149 lines
4.8 KiB
Swift

//
// BlackjackTableView.swift
// Blackjack
//
// The main table layout showing dealer and player hands.
//
import SwiftUI
import CasinoKit
struct BlackjackTableView: View {
@Bindable var state: GameState
let onPlaceBet: () -> Void
/// Whether to show Hi-Lo card count values on cards.
var showCardCount: Bool { state.settings.showCardCount }
// MARK: - Scaled Metrics
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
@ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge
@ScaledMetric(relativeTo: .caption) private var hintFontSize: CGFloat = Design.BaseFontSize.small
// MARK: - Layout
private let cardWidth: CGFloat = Design.Size.cardWidth
private let cardSpacing: CGFloat = Design.Size.cardOverlap
var body: some View {
VStack(spacing: Design.Spacing.large) {
// Dealer area
DealerHandView(
hand: state.dealerHand,
showHoleCard: shouldShowDealerHoleCard,
showCardCount: showCardCount,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
Spacer()
// Player hands area - only show when there are cards dealt
if state.playerHands.first?.cards.isEmpty == false {
PlayerHandsView(
hands: state.playerHands,
activeHandIndex: state.activeHandIndex,
isPlayerTurn: isPlayerTurn,
showCardCount: showCardCount,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.transition(.opacity)
}
// Betting zone (when betting)
if state.currentPhase == .betting {
BettingZoneView(
betAmount: state.currentBet,
minBet: state.settings.minBet,
maxBet: state.settings.maxBet,
onTap: onPlaceBet
)
.transition(.scale.combined(with: .opacity))
// Betting hint based on count (only when card counting enabled)
if showCardCount, let bettingHint = bettingHint {
BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount)
.transition(.opacity)
}
}
// Hint (when enabled and player turn)
if state.settings.showHints && isPlayerTurn, let hint = currentHint {
HintView(hint: hint)
.transition(.opacity)
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
}
// MARK: - Computed Properties
private var shouldShowDealerHoleCard: Bool {
switch state.currentPhase {
case .dealerTurn, .roundComplete:
return true
default:
return false
}
}
private var isPlayerTurn: Bool {
if case .playerTurn = state.currentPhase {
return true
}
return false
}
private var currentHint: String? {
guard let hand = state.activeHand,
let upCard = state.dealerUpCard else { return nil }
// Use count-adjusted hints when card counting is enabled
if showCardCount {
return state.engine.getCountAdjustedHint(playerHand: hand, dealerUpCard: upCard)
}
return state.engine.getHint(playerHand: hand, dealerUpCard: upCard)
}
/// Betting recommendation based on the true count.
private var bettingHint: String? {
let tc = Int(state.engine.trueCount.rounded())
// Betting spread recommendations based on true count
switch tc {
case ...(-2):
return String(localized: "Bet minimum or sit out")
case -1:
return String(localized: "Bet minimum")
case 0:
return String(localized: "Bet minimum (neutral)")
case 1:
return String(localized: "Bet 2x minimum")
case 2:
return String(localized: "Bet 4x minimum")
case 3:
return String(localized: "Bet 6x minimum")
case 4:
return String(localized: "Bet 8x minimum")
case 5...:
return String(localized: "Bet maximum!")
default:
return nil
}
}
}
// MARK: - Previews
#Preview {
ZStack {
Color.Table.felt.ignoresSafeArea()
Text("Use GameTableView for full preview")
.foregroundStyle(.white)
}
}