CasinoGames/CasinoKit/Sources/CasinoKit/Views/Cards/HandDisplayView.swift
Matt Bruce 78f7cb1544 Reorganize repo: move git to root, add Blackjack and CasinoKit
- Move .git from Baccarat/ to CasinoGames root
- Add Blackjack game project
- Add CasinoKit shared framework
- Add .gitignore for Xcode/Swift projects
- Add Agents.md and GAME_TEMPLATE.md documentation
- Add CasinoGames.xcworkspace
2025-12-22 13:18:29 -06:00

195 lines
6.5 KiB
Swift

//
// HandDisplayView.swift
// CasinoKit
//
// A generic view for displaying a hand of cards with optional overlap.
//
import SwiftUI
/// A view displaying a hand of cards with configurable layout.
public struct HandDisplayView: View {
/// The cards in the hand.
public let cards: [Card]
/// Which cards are face up (by index).
public let cardsFaceUp: [Bool]
/// The width of each card.
public let cardWidth: CGFloat
/// The overlap between cards (negative = overlap, positive = gap).
public let cardSpacing: CGFloat
/// Whether this hand is the winner.
public let isWinner: Bool
/// Optional label to show (e.g., "PLAYER", "DEALER").
public let label: String?
/// Optional value badge to show.
public let value: Int?
/// Badge color for the value.
public let valueColor: Color
/// Maximum number of card slots to reserve space for.
public let maxCards: Int
// Layout
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .caption) private var winBadgeFontSize: CGFloat = 10
/// Creates a hand display view.
/// - Parameters:
/// - cards: The cards to display.
/// - cardsFaceUp: Which cards are face up.
/// - cardWidth: Width of each card.
/// - cardSpacing: Spacing between cards (negative for overlap).
/// - isWinner: Whether to show winner styling.
/// - label: Optional label above cards.
/// - value: Optional value badge to show.
/// - valueColor: Color for value badge.
/// - maxCards: Max cards to reserve space for (default: 3).
public init(
cards: [Card],
cardsFaceUp: [Bool] = [],
cardWidth: CGFloat = 45,
cardSpacing: CGFloat = -12,
isWinner: Bool = false,
label: String? = nil,
value: Int? = nil,
valueColor: Color = .blue,
maxCards: Int = 3
) {
self.cards = cards
self.cardsFaceUp = cardsFaceUp
self.cardWidth = cardWidth
self.cardSpacing = cardSpacing
self.isWinner = isWinner
self.label = label
self.value = value
self.valueColor = valueColor
self.maxCards = maxCards
}
/// Card height based on aspect ratio.
private var cardHeight: CGFloat {
cardWidth * CasinoDesign.Size.cardAspectRatio
}
/// Fixed container width based on max cards.
private var containerWidth: CGFloat {
if maxCards <= 1 {
return cardWidth + CasinoDesign.Spacing.xSmall * 2
}
let cardsWidth = cardWidth + (cardWidth + cardSpacing) * CGFloat(maxCards - 1)
return cardsWidth + CasinoDesign.Spacing.xSmall * 2
}
/// Fixed container height.
private var containerHeight: CGFloat {
cardHeight + CasinoDesign.Spacing.xSmall * 2
}
public var body: some View {
VStack(spacing: CasinoDesign.Spacing.small) {
// Label with optional value badge
if label != nil || value != nil {
HStack(spacing: CasinoDesign.Spacing.small) {
if let label = label {
Text(label)
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white)
}
if let value = value, !cards.isEmpty {
ValueBadge(value: value, color: valueColor)
}
}
.frame(minHeight: CasinoDesign.Spacing.xxxLarge)
}
// Cards container
ZStack {
// Fixed-size container
Color.clear
.frame(width: containerWidth, height: containerHeight)
// Cards
HStack(spacing: cards.isEmpty ? CasinoDesign.Spacing.small : cardSpacing) {
if cards.isEmpty {
// Placeholders
ForEach(0..<min(2, maxCards), id: \.self) { _ in
CardPlaceholderView(width: cardWidth)
}
} else {
ForEach(cards.indices, id: \.self) { index in
let isFaceUp = index < cardsFaceUp.count ? cardsFaceUp[index] : true
CardView(
card: cards[index],
isFaceUp: isFaceUp,
cardWidth: cardWidth
)
.zIndex(Double(index))
}
}
}
}
.background(
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
.strokeBorder(
isWinner ? Color.yellow : Color.clear,
lineWidth: CasinoDesign.LineWidth.medium
)
)
.overlay(alignment: .bottom) {
if isWinner {
Text(String(localized: "WIN", bundle: .module))
.font(.system(size: winBadgeFontSize, weight: .black))
.foregroundStyle(.black)
.padding(.horizontal, CasinoDesign.Spacing.small)
.padding(.vertical, CasinoDesign.Spacing.xxSmall)
.background(
Capsule()
.fill(Color.yellow)
)
.offset(y: CasinoDesign.Spacing.small)
}
}
}
}
}
#Preview {
ZStack {
Color.CasinoTable.felt.ignoresSafeArea()
HStack(spacing: 40) {
HandDisplayView(
cards: [
Card(suit: .hearts, rank: .ace),
Card(suit: .spades, rank: .king)
],
cardsFaceUp: [true, true],
isWinner: true,
label: "PLAYER",
value: 21,
valueColor: .blue
)
HandDisplayView(
cards: [
Card(suit: .diamonds, rank: .seven),
Card(suit: .clubs, rank: .ten)
],
cardsFaceUp: [true, false],
label: "DEALER",
value: 17,
valueColor: .red
)
}
}
}