CasinoGames/Baccarat/Baccarat/Views/Table/CardsDisplayArea.swift

248 lines
8.0 KiB
Swift

//
// CardsDisplayArea.swift
// Baccarat
//
// The cards display area showing both Player and Banker hands.
//
import SwiftUI
import CasinoKit
/// The cards display area showing both hands.
struct CardsDisplayArea: View {
let playerCards: [Card]
let bankerCards: [Card]
let playerCardsFaceUp: [Bool]
let bankerCardsFaceUp: [Bool]
let playerValue: Int
let bankerValue: Int
let playerIsWinner: Bool
let bankerIsWinner: Bool
let isTie: Bool
/// Screen width for responsive card sizing
var screenWidth: CGFloat = 400
// MARK: - Environment
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
// MARK: - Computed Properties
/// Whether we're on a large screen (iPad)
private var isLargeScreen: Bool {
horizontalSizeClass == .regular
}
// Use global debug flag from Design constants
private var showDebugBorders: Bool { Design.showDebugBorders }
/// Label font size - only scales on iPad to avoid clipping on small iPhones
private var labelFontSize: CGFloat {
let baseSize: CGFloat = 14
return isLargeScreen ? baseSize * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseSize
}
/// Minimum height for label row - only scales on iPad
private var labelRowMinHeight: CGFloat {
let baseHeight: CGFloat = 30
return isLargeScreen ? baseHeight * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseHeight
}
/// Spacing between PLAYER and BANKER hands - reduced on smaller screens
private var handsSpacing: CGFloat {
isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large
}
/// Horizontal padding inside the container
private var containerPaddingH: CGFloat {
isLargeScreen ? Design.Spacing.xLarge : Design.Spacing.medium
}
/// Outer horizontal padding
private var outerPaddingH: CGFloat {
isLargeScreen ? Design.Spacing.large : Design.Spacing.small
}
// MARK: - Accessibility
private var playerHandDescription: String {
if playerCards.isEmpty {
return String(localized: "No cards")
}
let visibleCards = zip(playerCards, playerCardsFaceUp)
.filter { $1 }
.map { "\($0.0.rank.accessibilityName) of \($0.0.suit.accessibilityName)" }
if visibleCards.isEmpty {
return String(localized: "Cards face down")
}
let format = String(localized: "handValueFormat")
return visibleCards.joined(separator: ", ") + ". " + String(format: format, playerValue)
}
private var bankerHandDescription: String {
if bankerCards.isEmpty {
return String(localized: "No cards")
}
let visibleCards = zip(bankerCards, bankerCardsFaceUp)
.filter { $1 }
.map { "\($0.0.rank.accessibilityName) of \($0.0.suit.accessibilityName)" }
if visibleCards.isEmpty {
return String(localized: "Cards face down")
}
let format = String(localized: "handValueFormat")
return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue)
}
// MARK: - Body
var body: some View {
HStack(spacing: handsSpacing) {
// Player side
playerHandSection
.debugBorder(showDebugBorders, color: .blue, label: "Player")
// Banker side
bankerHandSection
.debugBorder(showDebugBorders, color: .red, label: "Banker")
}
.padding(.top, Design.Spacing.medium)
.padding(.bottom, Design.Spacing.large)
.padding(.horizontal, containerPaddingH)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.black.opacity(Design.Opacity.quarter))
.accessibilityHidden(true)
)
.padding(.horizontal, outerPaddingH)
.debugBorder(showDebugBorders, color: .mint, label: "HandsContainer")
}
// MARK: - Private Views
private var playerHandSection: some View {
VStack(spacing: Design.Spacing.small) {
// Label with value
HStack(spacing: Design.Spacing.small) {
Text("PLAYER")
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white)
if !playerCards.isEmpty && playerCardsFaceUp.contains(true) {
HandValueBadge(value: playerValue, color: .blue)
}
}
.frame(minHeight: labelRowMinHeight)
// Cards
CompactHandView(
cards: playerCards,
cardsFaceUp: playerCardsFaceUp,
isWinner: playerIsWinner,
screenWidth: screenWidth
)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Player hand"))
.accessibilityValue(playerHandDescription + (playerIsWinner ? ", " + String(localized: "Winner") : ""))
}
private var bankerHandSection: some View {
VStack(spacing: Design.Spacing.small) {
// Label with value
HStack(spacing: Design.Spacing.small) {
Text("BANKER")
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white)
if !bankerCards.isEmpty && bankerCardsFaceUp.contains(true) {
HandValueBadge(value: bankerValue, color: .red)
}
}
.frame(minHeight: labelRowMinHeight)
// Cards
CompactHandView(
cards: bankerCards,
cardsFaceUp: bankerCardsFaceUp,
isWinner: bankerIsWinner,
screenWidth: screenWidth
)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Banker hand"))
.accessibilityValue(bankerHandDescription + (bankerIsWinner ? ", " + String(localized: "Winner") : ""))
}
}
// MARK: - Previews
#Preview("Empty Hands") {
ZStack {
TableBackgroundView()
CardsDisplayArea(
playerCards: [],
bankerCards: [],
playerCardsFaceUp: [],
bankerCardsFaceUp: [],
playerValue: 0,
bankerValue: 0,
playerIsWinner: false,
bankerIsWinner: false,
isTie: false
)
}
}
#Preview("Player Wins") {
ZStack {
TableBackgroundView()
CardsDisplayArea(
playerCards: [
Card(suit: .spades, rank: .king),
Card(suit: .hearts, rank: .eight)
],
bankerCards: [
Card(suit: .clubs, rank: .seven),
Card(suit: .diamonds, rank: .five)
],
playerCardsFaceUp: [true, true],
bankerCardsFaceUp: [true, true],
playerValue: 8,
bankerValue: 2,
playerIsWinner: true,
bankerIsWinner: false,
isTie: false
)
}
}
#Preview("Banker Wins with 3 Cards") {
ZStack {
TableBackgroundView()
CardsDisplayArea(
playerCards: [
Card(suit: .spades, rank: .four),
Card(suit: .hearts, rank: .three),
Card(suit: .clubs, rank: .two)
],
bankerCards: [
Card(suit: .hearts, rank: .ace),
Card(suit: .diamonds, rank: .ace),
Card(suit: .spades, rank: .seven)
],
playerCardsFaceUp: [true, true, true],
bankerCardsFaceUp: [true, true, true],
playerValue: 9,
bankerValue: 8,
playerIsWinner: false,
bankerIsWinner: true,
isTie: false
)
}
}