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

169 lines
6.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
// MARK: - Environment
@Environment(\.verticalSizeClass) private var verticalSizeClass
// MARK: - State
/// Screen dimensions measured from container for responsive sizing
@State private var screenHeight: CGFloat = 800
@State private var screenWidth: CGFloat = 375
/// 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: - Dynamic Card Sizing
/// Whether we're in landscape mode
/// - iPhones: use verticalSizeClass == .compact
/// - iPads: use screen dimensions (since iPads always report .regular)
private var isLandscape: Bool {
if DeviceInfo.isPad {
return screenWidth > screenHeight
}
return verticalSizeClass == .compact
}
/// Card width derived from screen height percentage and card aspect ratio
/// cardHeight = maxHeight × percentage, then cardWidth = cardHeight / aspectRatio
private var cardWidth: CGFloat {
let maxHeight = max(screenWidth, screenHeight)
let heightPercentage: CGFloat = 0.19 // Card takes 12% of screen height
let cardHeight = maxHeight * heightPercentage
return cardHeight
}
/// Card overlap scales proportionally with card width
private var cardSpacing: CGFloat {
// Overlap ratio: roughly -55% of card width
cardWidth * -0.55
}
/// 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.18 // 20% of height above baseline
let minSpacing: CGFloat = 10 // 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
)
.padding(.bottom, 5)
.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)
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { size in
screenWidth = size.width
screenHeight = size.height
}
.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)
}
}