653 lines
24 KiB
Swift
653 lines
24 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
|
|
|
|
/// 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
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.large) {
|
|
// Dealer area
|
|
DealerHandView(
|
|
hand: state.dealerHand,
|
|
showHoleCard: shouldShowDealerHoleCard,
|
|
showCardCount: showCardCount,
|
|
cardWidth: cardWidth,
|
|
cardSpacing: cardSpacing
|
|
)
|
|
|
|
Spacer()
|
|
|
|
// Insurance zone (when offered)
|
|
if state.currentPhase == .insurance {
|
|
InsuranceZoneView(
|
|
betAmount: state.currentBet / 2,
|
|
balance: state.balance,
|
|
onTake: { Task { await state.takeInsurance() } },
|
|
onDecline: { state.declineInsurance() }
|
|
)
|
|
.transition(.scale.combined(with: .opacity))
|
|
}
|
|
|
|
// Player hands area
|
|
PlayerHandsView(
|
|
hands: state.playerHands,
|
|
activeHandIndex: state.activeHandIndex,
|
|
isPlayerTurn: isPlayerTurn,
|
|
showCardCount: showCardCount,
|
|
cardWidth: cardWidth,
|
|
cardSpacing: cardSpacing
|
|
)
|
|
|
|
// Betting zone (when betting)
|
|
if state.currentPhase == .betting {
|
|
BettingZoneView(
|
|
betAmount: state.currentBet,
|
|
minBet: state.settings.minBet,
|
|
maxBet: state.settings.maxBet,
|
|
onTap: onPlaceBet
|
|
)
|
|
.transition(.scale.combined(with: .opacity))
|
|
|
|
// Betting hint based on count (only when card counting enabled)
|
|
if showCardCount, let bettingHint = bettingHint {
|
|
BettingHintView(hint: bettingHint, trueCount: state.engine.trueCount)
|
|
.transition(.opacity)
|
|
}
|
|
}
|
|
|
|
// Hint (when enabled and player turn)
|
|
if state.settings.showHints && isPlayerTurn, let hint = currentHint {
|
|
HintView(hint: hint)
|
|
.transition(.opacity)
|
|
}
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
.padding(.vertical, Design.Spacing.medium)
|
|
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
private var shouldShowDealerHoleCard: Bool {
|
|
switch state.currentPhase {
|
|
case .dealerTurn, .roundComplete:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private var isPlayerTurn: Bool {
|
|
if case .playerTurn = state.currentPhase {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private var currentHint: String? {
|
|
guard let hand = state.activeHand,
|
|
let upCard = state.dealerUpCard else { return nil }
|
|
|
|
// Use count-adjusted hints when card counting is enabled
|
|
if showCardCount {
|
|
return state.engine.getCountAdjustedHint(playerHand: hand, dealerUpCard: upCard)
|
|
}
|
|
return state.engine.getHint(playerHand: hand, dealerUpCard: upCard)
|
|
}
|
|
|
|
/// Betting recommendation based on the true count.
|
|
private var bettingHint: String? {
|
|
let tc = Int(state.engine.trueCount.rounded())
|
|
|
|
// Betting spread recommendations based on true count
|
|
switch tc {
|
|
case ...(-2):
|
|
return String(localized: "Bet minimum or sit out")
|
|
case -1:
|
|
return String(localized: "Bet minimum")
|
|
case 0:
|
|
return String(localized: "Bet minimum (neutral)")
|
|
case 1:
|
|
return String(localized: "Bet 2x minimum")
|
|
case 2:
|
|
return String(localized: "Bet 4x minimum")
|
|
case 3:
|
|
return String(localized: "Bet 6x minimum")
|
|
case 4:
|
|
return String(localized: "Bet 8x minimum")
|
|
case 5...:
|
|
return String(localized: "Bet maximum!")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Dealer Hand View
|
|
|
|
struct DealerHandView: View {
|
|
let hand: BlackjackHand
|
|
let showHoleCard: Bool
|
|
let showCardCount: Bool
|
|
let cardWidth: CGFloat
|
|
let cardSpacing: CGFloat
|
|
|
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
// Label and value
|
|
HStack(spacing: Design.Spacing.small) {
|
|
Text(String(localized: "DEALER"))
|
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
// Show value: always show if hole card visible, or show single card value in European mode
|
|
if !hand.cards.isEmpty {
|
|
if showHoleCard {
|
|
ValueBadge(value: hand.value, color: Color.Hand.dealer)
|
|
} else if hand.cards.count == 1 {
|
|
// European mode: show single visible card value
|
|
ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cards
|
|
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
|
if hand.cards.isEmpty {
|
|
CardPlaceholderView(width: cardWidth)
|
|
CardPlaceholderView(width: cardWidth)
|
|
} else {
|
|
ForEach(hand.cards.indices, id: \.self) { index in
|
|
let isFaceUp = index == 0 || showHoleCard
|
|
CardView(
|
|
card: hand.cards[index],
|
|
isFaceUp: isFaceUp,
|
|
cardWidth: cardWidth
|
|
)
|
|
.overlay(alignment: .bottomLeading) {
|
|
if showCardCount && isFaceUp {
|
|
HiLoCountBadge(card: hand.cards[index])
|
|
}
|
|
}
|
|
.zIndex(Double(index))
|
|
}
|
|
|
|
// Show placeholder for second card in European mode (no hole card)
|
|
if hand.cards.count == 1 && !showHoleCard {
|
|
CardPlaceholderView(width: cardWidth)
|
|
.opacity(Design.Opacity.medium)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Result badge
|
|
if let result = hand.cards.count >= 2 && showHoleCard ? handResultText : nil {
|
|
Text(result)
|
|
.font(.system(size: labelFontSize, weight: .black))
|
|
.foregroundStyle(handResultColor)
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
.background(
|
|
Capsule()
|
|
.fill(handResultColor.opacity(Design.Opacity.hint))
|
|
)
|
|
}
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(dealerAccessibilityLabel)
|
|
}
|
|
|
|
private var handResultText: String? {
|
|
if hand.isBlackjack {
|
|
return String(localized: "BLACKJACK")
|
|
}
|
|
if hand.isBusted {
|
|
return String(localized: "BUST")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private var handResultColor: Color {
|
|
if hand.isBlackjack { return .yellow }
|
|
if hand.isBusted { return .green } // Good for player
|
|
return .white
|
|
}
|
|
|
|
private var dealerAccessibilityLabel: String {
|
|
if hand.cards.isEmpty {
|
|
return String(localized: "Dealer: No cards")
|
|
}
|
|
let visibleCards = showHoleCard ? hand.cards : [hand.cards[0]]
|
|
let cardsDescription = visibleCards.map { $0.accessibilityDescription }.joined(separator: ", ")
|
|
return String(localized: "Dealer: \(cardsDescription). Value: \(showHoleCard ? String(hand.value) : "hidden")")
|
|
}
|
|
}
|
|
|
|
// MARK: - Player Hands View
|
|
|
|
struct PlayerHandsView: View {
|
|
let hands: [BlackjackHand]
|
|
let activeHandIndex: Int
|
|
let isPlayerTurn: Bool
|
|
let showCardCount: Bool
|
|
let cardWidth: CGFloat
|
|
let cardSpacing: CGFloat
|
|
|
|
/// Adaptive card width based on number of hands (smaller cards for more splits)
|
|
private var adaptiveCardWidth: CGFloat {
|
|
switch hands.count {
|
|
case 1, 2:
|
|
return cardWidth
|
|
case 3:
|
|
return cardWidth * 0.90
|
|
default: // 4+ hands
|
|
return cardWidth * 0.85
|
|
}
|
|
}
|
|
|
|
/// Adaptive spacing based on number of hands
|
|
private var adaptiveSpacing: CGFloat {
|
|
switch hands.count {
|
|
case 1, 2:
|
|
return Design.Spacing.xxLarge
|
|
case 3:
|
|
return Design.Spacing.large
|
|
default: // 4+ hands
|
|
return Design.Spacing.medium
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: adaptiveSpacing) {
|
|
// Display hands in reverse order (right to left play order)
|
|
// So hand 0 (played first) appears on the right
|
|
ForEach(hands.indices.reversed(), id: \.self) { index in
|
|
PlayerHandView(
|
|
hand: hands[index],
|
|
isActive: index == activeHandIndex && isPlayerTurn,
|
|
showCardCount: showCardCount,
|
|
// Hand numbers: rightmost is Hand 1, leftmost is Hand 2, etc.
|
|
handNumber: hands.count > 1 ? hands.count - index : nil,
|
|
cardWidth: adaptiveCardWidth,
|
|
cardSpacing: cardSpacing
|
|
)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity) // Center the hands horizontally
|
|
}
|
|
}
|
|
|
|
struct PlayerHandView: View {
|
|
let hand: BlackjackHand
|
|
let isActive: Bool
|
|
let showCardCount: Bool
|
|
let handNumber: Int?
|
|
let cardWidth: CGFloat
|
|
let cardSpacing: CGFloat
|
|
|
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
|
|
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.BaseFontSize.small
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
// Cards with container - uses dynamic sizing based on card count
|
|
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
|
if hand.cards.isEmpty {
|
|
CardPlaceholderView(width: cardWidth)
|
|
CardPlaceholderView(width: cardWidth)
|
|
} else {
|
|
ForEach(hand.cards.indices, id: \.self) { index in
|
|
CardView(
|
|
card: hand.cards[index],
|
|
isFaceUp: true,
|
|
cardWidth: cardWidth
|
|
)
|
|
.overlay(alignment: .bottomLeading) {
|
|
if showCardCount {
|
|
HiLoCountBadge(card: hand.cards[index])
|
|
}
|
|
}
|
|
.zIndex(Double(index))
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
.padding(.vertical, Design.Spacing.medium)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
.fill(Color.Table.feltDark.opacity(Design.Opacity.light))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
.strokeBorder(
|
|
isActive ? Color.Hand.active : Color.white.opacity(Design.Opacity.hint),
|
|
lineWidth: isActive ? Design.LineWidth.thick : Design.LineWidth.thin
|
|
)
|
|
)
|
|
)
|
|
.contentShape(Rectangle()) // Ensure tap area matches visual
|
|
.animation(.easeInOut(duration: Design.Animation.quick), value: isActive)
|
|
|
|
// Hand info
|
|
HStack(spacing: Design.Spacing.small) {
|
|
if let number = handNumber {
|
|
Text(String(localized: "Hand \(number)"))
|
|
.font(.system(size: handNumberSize, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
}
|
|
|
|
if !hand.cards.isEmpty {
|
|
Text(hand.valueDisplay)
|
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
|
.foregroundStyle(valueColor)
|
|
}
|
|
|
|
if hand.isDoubledDown {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: handNumberSize))
|
|
.foregroundStyle(.purple)
|
|
}
|
|
}
|
|
|
|
// Result badge
|
|
if let result = hand.result {
|
|
Text(result.displayText)
|
|
.font(.system(size: labelFontSize, weight: .black))
|
|
.foregroundStyle(result.color)
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
.background(
|
|
Capsule()
|
|
.fill(result.color.opacity(Design.Opacity.hint))
|
|
)
|
|
}
|
|
|
|
// Bet amount
|
|
if hand.bet > 0 {
|
|
HStack(spacing: Design.Spacing.xSmall) {
|
|
Image(systemName: "dollarsign.circle.fill")
|
|
.foregroundStyle(.yellow)
|
|
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
|
|
.font(.system(size: handNumberSize, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.yellow)
|
|
}
|
|
}
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(playerAccessibilityLabel)
|
|
}
|
|
|
|
private var valueColor: Color {
|
|
if hand.isBlackjack { return .yellow }
|
|
if hand.isBusted { return .red }
|
|
if hand.value == 21 { return .green }
|
|
return .white
|
|
}
|
|
|
|
private var playerAccessibilityLabel: String {
|
|
let cardsDescription = hand.cards.map { $0.accessibilityDescription }.joined(separator: ", ")
|
|
var label = String(localized: "Player hand: \(cardsDescription). Value: \(hand.valueDisplay)")
|
|
if let result = hand.result {
|
|
label += ". \(result.displayText)"
|
|
}
|
|
return label
|
|
}
|
|
}
|
|
|
|
// MARK: - Betting Zone View
|
|
|
|
struct BettingZoneView: View {
|
|
let betAmount: Int
|
|
let minBet: Int
|
|
let maxBet: Int
|
|
let onTap: () -> Void
|
|
|
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.large
|
|
|
|
private var isAtMax: Bool {
|
|
betAmount >= maxBet
|
|
}
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
ZStack {
|
|
// Background
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.BettingZone.main)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.strokeBorder(Color.BettingZone.mainBorder, lineWidth: Design.LineWidth.medium)
|
|
)
|
|
|
|
// Content
|
|
if betAmount > 0 {
|
|
// Show chip with amount
|
|
ChipOnTableView(amount: betAmount, showMax: isAtMax)
|
|
} else {
|
|
// Empty state
|
|
VStack(spacing: Design.Spacing.small) {
|
|
Text(String(localized: "TAP TO BET"))
|
|
.font(.system(size: labelFontSize, weight: .bold))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
Text(String(localized: "Min: $\(minBet)"))
|
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
|
|
|
Text(String(localized: "Max: $\(maxBet.formatted())"))
|
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: Design.Size.bettingZoneHeight)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet")
|
|
.accessibilityHint("Double tap to add chips")
|
|
}
|
|
}
|
|
|
|
// MARK: - Insurance Zone View
|
|
|
|
struct InsuranceZoneView: View {
|
|
let betAmount: Int
|
|
let balance: Int
|
|
let onTake: () -> Void
|
|
let onDecline: () -> Void
|
|
|
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
|
|
@ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = Design.BaseFontSize.body
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
Text(String(localized: "INSURANCE?"))
|
|
.font(.system(size: labelFontSize, weight: .bold))
|
|
.foregroundStyle(.yellow)
|
|
|
|
Text(String(localized: "Dealer showing Ace"))
|
|
.font(.system(size: Design.BaseFontSize.small))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
|
|
HStack(spacing: Design.Spacing.large) {
|
|
Button(action: onDecline) {
|
|
Text(String(localized: "No"))
|
|
.font(.system(size: buttonFontSize, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
|
.padding(.vertical, Design.Spacing.medium)
|
|
.background(
|
|
Capsule()
|
|
.fill(Color.Button.surrender)
|
|
)
|
|
}
|
|
|
|
if balance >= betAmount {
|
|
Button(action: onTake) {
|
|
Text(String(localized: "Yes ($\(betAmount))"))
|
|
.font(.system(size: buttonFontSize, weight: .bold))
|
|
.foregroundStyle(.black)
|
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
|
.padding(.vertical, Design.Spacing.medium)
|
|
.background(
|
|
Capsule()
|
|
.fill(Color.Button.insurance)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.BettingZone.insurance.opacity(Design.Opacity.heavy))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.strokeBorder(Color.BettingZone.insuranceBorder, lineWidth: Design.LineWidth.medium)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Betting Hint View
|
|
|
|
/// Shows betting recommendations based on the current count.
|
|
struct BettingHintView: View {
|
|
let hint: String
|
|
let trueCount: Double
|
|
|
|
private var hintColor: Color {
|
|
let tc = Int(trueCount.rounded())
|
|
if tc >= 2 {
|
|
return .green // Player advantage - bet more
|
|
} else if tc <= -1 {
|
|
return .red // House advantage - bet less
|
|
} else {
|
|
return .yellow // Neutral
|
|
}
|
|
}
|
|
|
|
private var icon: String {
|
|
let tc = Int(trueCount.rounded())
|
|
if tc >= 2 {
|
|
return "arrow.up.circle.fill" // Increase bet
|
|
} else if tc <= -1 {
|
|
return "arrow.down.circle.fill" // Decrease bet
|
|
} else {
|
|
return "equal.circle.fill" // Neutral
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: Design.Spacing.small) {
|
|
Image(systemName: icon)
|
|
.foregroundStyle(hintColor)
|
|
Text(hint)
|
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
}
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
.padding(.vertical, Design.Spacing.small)
|
|
.background(
|
|
Capsule()
|
|
.fill(Color.black.opacity(Design.Opacity.light))
|
|
.overlay(
|
|
Capsule()
|
|
.strokeBorder(hintColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
|
)
|
|
)
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(String(localized: "Betting Hint"))
|
|
.accessibilityValue(hint)
|
|
}
|
|
}
|
|
|
|
// MARK: - Hint View
|
|
|
|
struct HintView: View {
|
|
let hint: String
|
|
|
|
var body: some View {
|
|
HStack(spacing: Design.Spacing.small) {
|
|
Image(systemName: "lightbulb.fill")
|
|
.foregroundStyle(.yellow)
|
|
Text(String(localized: "Hint: \(hint)"))
|
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
}
|
|
.padding(.horizontal, Design.Spacing.medium)
|
|
.padding(.vertical, Design.Spacing.small)
|
|
.background(
|
|
Capsule()
|
|
.fill(Color.black.opacity(Design.Opacity.light))
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Hi-Lo Count Badge
|
|
|
|
/// A small badge showing the Hi-Lo counting value of a card.
|
|
struct HiLoCountBadge: View {
|
|
let card: Card
|
|
|
|
var body: some View {
|
|
Text(card.hiLoDisplayText)
|
|
.font(.system(size: Design.BaseFontSize.xxSmall, weight: .bold, design: .rounded))
|
|
.foregroundStyle(badgeTextColor)
|
|
.padding(.horizontal, Design.Spacing.xSmall)
|
|
.padding(.vertical, Design.Spacing.xxxSmall)
|
|
.background(
|
|
Capsule()
|
|
.fill(badgeBackgroundColor)
|
|
)
|
|
.offset(x: -Design.Spacing.xSmall, y: Design.Spacing.xSmall)
|
|
}
|
|
|
|
private var badgeBackgroundColor: Color {
|
|
switch card.hiLoValue {
|
|
case 1: return .green // Low cards = positive for player
|
|
case -1: return .red // High cards = negative for player
|
|
default: return .gray // Neutral
|
|
}
|
|
}
|
|
|
|
private var badgeTextColor: Color {
|
|
.white
|
|
}
|
|
}
|
|
|
|
// MARK: - Card Accessibility Extension
|
|
|
|
extension Card {
|
|
var accessibilityDescription: String {
|
|
"\(rank.accessibilityName) of \(suit.accessibilityName)"
|
|
}
|
|
}
|
|
|