- 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
195 lines
6.5 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|