CasinoGames/Blackjack/Blackjack/Views/Table/BlackjackTableView.swift

133 lines
5.2 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
/// Screen height passed from parent for responsive sizing
var screenHeight: CGFloat = 800
/// 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
/// Fixed height for the hint area to prevent layout shifts
private let hintAreaHeight: CGFloat = 44
// Use global debug flag from Design constants
private var showDebugBorders: Bool { Design.showDebugBorders }
/// Dynamic spacer height based on screen size.
/// Formula: spacing = clamp((screenHeight - baseline) * scale, min, max)
/// This produces smooth scaling across all device sizes:
/// - iPhone SE (~667pt): ~20pt
/// - iPhone Pro Max (~932pt): ~76pt
/// - iPad Mini (~1024pt): ~95pt
/// - iPad Pro 12.9" (~1366pt): ~150pt (capped)
private var dealerPlayerSpacing: CGFloat {
let baseline: CGFloat = 550 // Below this, use minimum
let scale: CGFloat = 0.2 // 20% of height above baseline
let minSpacing: CGFloat = 20 // Floor for smallest screens
let maxSpacing: CGFloat = 150 // Ceiling for largest screens
let calculated = (screenHeight - baseline) * scale
return min(maxSpacing, max(minSpacing, calculated))
}
var body: some View {
VStack(spacing: Design.Spacing.small) {
// Dealer area
DealerHandView(
hand: state.dealerHand,
showHoleCard: state.shouldShowDealerHoleCard,
showCardCount: showCardCount,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
// Flexible space between dealer and player - scales with screen size
Spacer(minLength: dealerPlayerSpacing)
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer \(Int(dealerPlayerSpacing))")
// 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: state.isPlayerTurn,
showCardCount: showCardCount,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.transition(.opacity)
.debugBorder(showDebugBorders, color: .green, label: "Player")
}
// Betting zone (when betting)
if state.currentPhase == .betting {
Spacer()
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer2")
BettingZoneView(
betAmount: state.currentBet,
minBet: state.settings.minBet,
maxBet: state.settings.maxBet,
onTap: onPlaceBet
)
.transition(.scale.combined(with: .opacity))
.debugBorder(showDebugBorders, color: .blue, label: "BetZone")
// Betting hint based on count (only when card counting enabled)
if let hint = state.bettingHint {
BettingHintView(hint: hint, trueCount: state.engine.trueCount)
.transition(.opacity)
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
}
} else {
// Fixed-height hint area to prevent layout shifts during player turn
ZStack {
if let hint = state.currentHint {
HintView(hint: hint)
.transition(.opacity)
}
}
.frame(height: hintAreaHeight)
.debugBorder(showDebugBorders, color: .orange, label: "HintArea")
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.debugBorder(showDebugBorders, color: .white, label: "TableView")
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
}
}
// MARK: - Previews
#Preview {
ZStack {
Color.Table.felt.ignoresSafeArea()
Text("Use GameTableView for full preview")
.foregroundStyle(.white)
}
}