From 50f569d1376c093dbd7a455dba1f7490658865bb Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 22 Dec 2025 09:33:41 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- Blackjack/Resources/Localizable.xcstrings | 22 +++++ Blackjack/Theme/DesignConstants.swift | 40 ++++++++- Blackjack/Views/BettingZoneView.swift | 12 +-- Blackjack/Views/BlackjackTableView.swift | 21 +++-- Blackjack/Views/DealerHandView.swift | 2 +- Blackjack/Views/GameTableView.swift | 85 ++++++++++++------- Blackjack/Views/HiLoCountBadge.swift | 8 +- Blackjack/Views/HintViews.swift | 14 +-- Blackjack/Views/PlayerHandView.swift | 13 +-- .../CasinoKit/Views/Chips/ChipStackView.swift | 21 +++-- 10 files changed, 164 insertions(+), 74 deletions(-) diff --git a/Blackjack/Resources/Localizable.xcstrings b/Blackjack/Resources/Localizable.xcstrings index 25b8905..9e31dfb 100644 --- a/Blackjack/Resources/Localizable.xcstrings +++ b/Blackjack/Resources/Localizable.xcstrings @@ -719,6 +719,28 @@ } } }, + "Add $%lld more to meet minimum" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add $%lld more to meet minimum" + } + }, + "es-MX" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añade $%lld más para alcanzar el mínimo" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajoutez %lld$ de plus pour atteindre le minimum" + } + } + } + }, "Clear" : { "localizations" : { "en" : { diff --git a/Blackjack/Theme/DesignConstants.swift b/Blackjack/Theme/DesignConstants.swift index b59f1fc..68a9ae8 100644 --- a/Blackjack/Theme/DesignConstants.swift +++ b/Blackjack/Theme/DesignConstants.swift @@ -31,10 +31,44 @@ enum Design { // MARK: - Blackjack-Specific Component Sizes enum Size { - // Cards - slightly larger than medium for better visibility - static let cardWidth: CGFloat = 60 + // Hand scaling factor (1.5 = 50% larger hands) + static let handScale: CGFloat = 1.5 + + // Cards - scaled for better visibility + static let cardWidth: CGFloat = 60 * handScale // 90pt at 1.5x static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall - static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap + static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap + + // Player hands container height (accommodates larger cards) + static let playerHandsHeight: CGFloat = 180 * handScale // 270pt at 1.5x + + // Hand label font sizes (scaled) + static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale + static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale + static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale + + // Hint font size (scaled to match hands) + static let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale + static let hintIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale + static let hintPaddingH: CGFloat = CasinoDesign.Spacing.medium * handScale + static let hintPaddingV: CGFloat = CasinoDesign.Spacing.small * handScale + + // Hand icons (scaled) + static let handIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale + + // Hi-Lo count badge (scaled) + static let countBadgeFontSize: CGFloat = CasinoDesign.BaseFontSize.xxSmall * handScale + static let countBadgePaddingH: CGFloat = CasinoDesign.Spacing.xSmall * handScale + static let countBadgePaddingV: CGFloat = CasinoDesign.Spacing.xxxSmall * handScale + static let countBadgeOffset: CGFloat = CasinoDesign.Spacing.xSmall * handScale + + // Betting zone (chip scales, but zone height stays reasonable) + static let bettingChipSize: CGFloat = 36 * handScale // 54pt at 1.5x + static let bettingZoneHeightScaled: CGFloat = CasinoDesign.Size.bettingZoneHeight // Keep original height to save space + + // Card count display (scaled) + static let cardCountLabelSize: CGFloat = CasinoDesign.BaseFontSize.xSmall * handScale + static let cardCountValueSize: CGFloat = CasinoDesign.BaseFontSize.large * handScale // Chips - use CasinoDesign values static let chipBadgeSize: CGFloat = CasinoDesign.Size.chipBadge diff --git a/Blackjack/Views/BettingZoneView.swift b/Blackjack/Views/BettingZoneView.swift index 658dde9..9dc0735 100644 --- a/Blackjack/Views/BettingZoneView.swift +++ b/Blackjack/Views/BettingZoneView.swift @@ -14,7 +14,7 @@ struct BettingZoneView: View { let maxBet: Int let onTap: () -> Void - @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.large + @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize private var isAtMax: Bool { betAmount >= maxBet @@ -33,8 +33,8 @@ struct BettingZoneView: View { // Content if betAmount > 0 { - // Show chip with amount - ChipOnTableView(amount: betAmount, showMax: isAtMax) + // Show chip with amount (scaled) + ChipOnTableView(amount: betAmount, showMax: isAtMax, size: Design.Size.bettingChipSize) } else { // Empty state VStack(spacing: Design.Spacing.small) { @@ -44,18 +44,18 @@ struct BettingZoneView: View { HStack(spacing: Design.Spacing.medium) { Text(String(localized: "Min: $\(minBet)")) - .font(.system(size: Design.BaseFontSize.small, weight: .medium)) + .font(.system(size: Design.Size.handNumberFontSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.light)) Text(String(localized: "Max: $\(maxBet.formatted())")) - .font(.system(size: Design.BaseFontSize.small, weight: .medium)) + .font(.system(size: Design.Size.handNumberFontSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.light)) } } } } .frame(maxWidth: .infinity) - .frame(height: Design.Size.bettingZoneHeight) + .frame(height: Design.Size.bettingZoneHeightScaled) } .buttonStyle(.plain) .accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet") diff --git a/Blackjack/Views/BlackjackTableView.swift b/Blackjack/Views/BlackjackTableView.swift index e07575b..4aa21a3 100644 --- a/Blackjack/Views/BlackjackTableView.swift +++ b/Blackjack/Views/BlackjackTableView.swift @@ -39,15 +39,18 @@ struct BlackjackTableView: View { Spacer() - // Player hands area - PlayerHandsView( - hands: state.playerHands, - activeHandIndex: state.activeHandIndex, - isPlayerTurn: isPlayerTurn, - showCardCount: showCardCount, - cardWidth: cardWidth, - cardSpacing: cardSpacing - ) + // 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: isPlayerTurn, + showCardCount: showCardCount, + cardWidth: cardWidth, + cardSpacing: cardSpacing + ) + .transition(.opacity) + } // Betting zone (when betting) if state.currentPhase == .betting { diff --git a/Blackjack/Views/DealerHandView.swift b/Blackjack/Views/DealerHandView.swift index 10bb127..03f385c 100644 --- a/Blackjack/Views/DealerHandView.swift +++ b/Blackjack/Views/DealerHandView.swift @@ -15,7 +15,7 @@ struct DealerHandView: View { let cardWidth: CGFloat let cardSpacing: CGFloat - @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium + @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize var body: some View { VStack(spacing: Design.Spacing.small) { diff --git a/Blackjack/Views/GameTableView.swift b/Blackjack/Views/GameTableView.swift index 6faff85..8e6699a 100644 --- a/Blackjack/Views/GameTableView.swift +++ b/Blackjack/Views/GameTableView.swift @@ -111,18 +111,18 @@ struct GameTableView: View { Spacer() - // Chip selector - only interactive during betting phase - // During gameplay, show chips as they were (balance + currentBet) but dimmed - ChipSelectorView( - selectedChip: $selectedChip, - balance: state.currentPhase == .betting ? state.balance : (state.balance + state.currentBet), - currentBet: state.currentPhase == .betting ? state.currentBet : 0, - maxBet: state.settings.maxBet - ) - .frame(maxWidth: maxContentWidth) - .padding(.bottom, Design.Spacing.small) - .opacity(state.currentPhase == .betting ? 1.0 : Design.Opacity.medium) - .allowsHitTesting(state.currentPhase == .betting) // Disable interaction when not betting + // Chip selector - only shown during betting phase + if state.currentPhase == .betting { + ChipSelectorView( + selectedChip: $selectedChip, + balance: state.balance, + currentBet: state.currentBet, + maxBet: state.settings.maxBet + ) + .frame(maxWidth: maxContentWidth) + .padding(.bottom, Design.Spacing.small) + .transition(.opacity.combined(with: .move(edge: .bottom))) + } // Action buttons ActionButtonsView(state: state) @@ -216,24 +216,47 @@ struct ActionButtonsView: View { // MARK: - Betting Phase Buttons + /// Whether the current bet meets the minimum requirement + private var isBetBelowMinimum: Bool { + state.currentBet > 0 && state.currentBet < state.settings.minBet + } + + /// Amount needed to reach minimum bet + private var amountNeededForMinimum: Int { + state.settings.minBet - state.currentBet + } + @ViewBuilder private var bettingButtons: some View { if state.currentBet > 0 { - ActionButton( - String(localized: "Clear"), - icon: "xmark.circle", - style: .destructive - ) { - state.clearBet() - } - - if state.canDeal { - ActionButton( - String(localized: "Deal"), - icon: "play.fill", - style: .primary - ) { - Task { await state.deal() } + VStack(spacing: Design.Spacing.small) { + // Show hint if bet is below minimum + if isBetBelowMinimum { + Text(String(localized: "Add $\(amountNeededForMinimum) more to meet minimum")) + .font(.system(size: Design.BaseFontSize.small, weight: .medium)) + .foregroundStyle(.orange) + .transition(.opacity) + } + + HStack(spacing: Design.Spacing.medium) { + ActionButton( + String(localized: "Clear"), + icon: "xmark.circle", + style: .destructive + ) { + state.clearBet() + } + + // Always show Deal button, but disable if below minimum + ActionButton( + String(localized: "Deal"), + icon: "play.fill", + style: .primary + ) { + Task { await state.deal() } + } + .opacity(state.canDeal ? 1.0 : Design.Opacity.medium) + .disabled(!state.canDeal) } } } @@ -379,11 +402,11 @@ struct CardCountView: View { // Running count VStack(spacing: Design.Spacing.xxSmall) { Text("Running") - .font(.system(size: Design.BaseFontSize.xSmall, weight: .medium)) + .font(.system(size: Design.Size.cardCountLabelSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)") - .font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .monospaced)) + .font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced)) .foregroundStyle(countColor(for: runningCount)) } @@ -394,11 +417,11 @@ struct CardCountView: View { // True count VStack(spacing: Design.Spacing.xxSmall) { Text("True") - .font(.system(size: Design.BaseFontSize.xSmall, weight: .medium)) + .font(.system(size: Design.Size.cardCountLabelSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) Text(trueCount >= 0 ? "+\(trueCount, format: .number.precision(.fractionLength(1)))" : "\(trueCount, format: .number.precision(.fractionLength(1)))") - .font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .monospaced)) + .font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced)) .foregroundStyle(countColor(for: Int(trueCount.rounded()))) } } diff --git a/Blackjack/Views/HiLoCountBadge.swift b/Blackjack/Views/HiLoCountBadge.swift index 57ce7a8..a525fed 100644 --- a/Blackjack/Views/HiLoCountBadge.swift +++ b/Blackjack/Views/HiLoCountBadge.swift @@ -14,15 +14,15 @@ struct HiLoCountBadge: View { var body: some View { Text(card.hiLoDisplayText) - .font(.system(size: Design.BaseFontSize.xxSmall, weight: .bold, design: .rounded)) + .font(.system(size: Design.Size.countBadgeFontSize, weight: .bold, design: .rounded)) .foregroundStyle(badgeTextColor) - .padding(.horizontal, Design.Spacing.xSmall) - .padding(.vertical, Design.Spacing.xxxSmall) + .padding(.horizontal, Design.Size.countBadgePaddingH) + .padding(.vertical, Design.Size.countBadgePaddingV) .background( Capsule() .fill(badgeBackgroundColor) ) - .offset(x: -Design.Spacing.xSmall, y: Design.Spacing.xSmall) + .offset(x: -Design.Size.countBadgeOffset, y: Design.Size.countBadgeOffset) } private var badgeBackgroundColor: Color { diff --git a/Blackjack/Views/HintViews.swift b/Blackjack/Views/HintViews.swift index 7ef87e5..e689109 100644 --- a/Blackjack/Views/HintViews.swift +++ b/Blackjack/Views/HintViews.swift @@ -17,13 +17,14 @@ struct HintView: View { var body: some View { HStack(spacing: Design.Spacing.small) { Image(systemName: "lightbulb.fill") + .font(.system(size: Design.Size.hintIconSize)) .foregroundStyle(.yellow) Text(String(localized: "Hint: \(hint)")) - .font(.system(size: Design.BaseFontSize.small, weight: .medium)) + .font(.system(size: Design.Size.hintFontSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.strong)) } - .padding(.horizontal, Design.Spacing.medium) - .padding(.vertical, Design.Spacing.small) + .padding(.horizontal, Design.Size.hintPaddingH) + .padding(.vertical, Design.Size.hintPaddingV) .background( Capsule() .fill(Color.black.opacity(Design.Opacity.light)) @@ -66,13 +67,14 @@ struct BettingHintView: View { var body: some View { HStack(spacing: Design.Spacing.small) { Image(systemName: icon) + .font(.system(size: Design.Size.hintIconSize)) .foregroundStyle(hintColor) Text(hint) - .font(.system(size: Design.BaseFontSize.small, weight: .medium)) + .font(.system(size: Design.Size.hintFontSize, weight: .medium)) .foregroundStyle(.white.opacity(Design.Opacity.strong)) } - .padding(.horizontal, Design.Spacing.medium) - .padding(.vertical, Design.Spacing.small) + .padding(.horizontal, Design.Size.hintPaddingH) + .padding(.vertical, Design.Size.hintPaddingV) .background( Capsule() .fill(Color.black.opacity(Design.Opacity.light)) diff --git a/Blackjack/Views/PlayerHandView.swift b/Blackjack/Views/PlayerHandView.swift index f1e4923..dfb9b1f 100644 --- a/Blackjack/Views/PlayerHandView.swift +++ b/Blackjack/Views/PlayerHandView.swift @@ -56,7 +56,7 @@ struct PlayerHandsView: View { } } } - .frame(height: 180) + .frame(height: Design.Size.playerHandsHeight) } } @@ -71,8 +71,8 @@ struct PlayerHandView: View { 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 + @ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize + @ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize var body: some View { VStack(spacing: Design.Spacing.small) { @@ -129,7 +129,7 @@ struct PlayerHandView: View { if hand.isDoubledDown { Image(systemName: "xmark.circle.fill") - .font(.system(size: handNumberSize)) + .font(.system(size: Design.Size.handIconSize)) .foregroundStyle(.purple) } } @@ -139,8 +139,8 @@ struct PlayerHandView: View { Text(result.displayText) .font(.system(size: labelFontSize, weight: .black)) .foregroundStyle(result.color) - .padding(.horizontal, Design.Spacing.medium) - .padding(.vertical, Design.Spacing.xSmall) + .padding(.horizontal, Design.Size.hintPaddingH) + .padding(.vertical, Design.Size.hintPaddingV) .background( Capsule() .fill(result.color.opacity(Design.Opacity.hint)) @@ -151,6 +151,7 @@ struct PlayerHandView: View { if hand.bet > 0 { HStack(spacing: Design.Spacing.xSmall) { Image(systemName: "dollarsign.circle.fill") + .font(.system(size: Design.Size.handIconSize)) .foregroundStyle(.yellow) Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))") .font(.system(size: handNumberSize, weight: .bold, design: .rounded)) diff --git a/CasinoKit/Sources/CasinoKit/Views/Chips/ChipStackView.swift b/CasinoKit/Sources/CasinoKit/Views/Chips/ChipStackView.swift index 9c67ec6..ce2c00d 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Chips/ChipStackView.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Chips/ChipStackView.swift @@ -62,26 +62,29 @@ public struct ChipStackView: View { public struct ChipOnTableView: View { let amount: Int let showMax: Bool + let size: CGFloat let theme: any CasinoTheme public init( amount: Int, showMax: Bool = false, + size: CGFloat = 36, theme: any CasinoTheme = DefaultCasinoTheme() ) { self.amount = amount self.showMax = showMax + self.size = size self.theme = theme } - // MARK: - Layout Constants + // MARK: - Layout Constants (relative to size) - private let chipSize: CGFloat = 36 - private let innerRingSize: CGFloat = 26 - private let gradientEndRadius: CGFloat = 20 - private let maxBadgeFontSize: CGFloat = 8 - private let maxBadgeOffsetX: CGFloat = 6 - private let maxBadgeOffsetY: CGFloat = -4 + private var chipSize: CGFloat { size } + private var innerRingSize: CGFloat { size * 0.72 } // 26/36 + private var gradientEndRadius: CGFloat { size * 0.56 } // 20/36 + private var maxBadgeFontSize: CGFloat { size * 0.22 } // 8/36 + private var maxBadgeOffsetX: CGFloat { size * 0.17 } // 6/36 + private var maxBadgeOffsetY: CGFloat { size * -0.11 } // -4/36 // MARK: - Computed Properties @@ -95,8 +98,10 @@ public struct ChipOnTableView: View { amount >= 1000 ? "\(amount / 1000)K" : "\(amount)" } + /// Text font size scales with chip size private var textFontSize: CGFloat { - amount >= 1000 ? CasinoDesign.BaseFontSize.xSmall : CasinoDesign.BaseFontSize.xSmall + 1 + let baseFontSize = amount >= 1000 ? size * 0.25 : size * 0.30 + return baseFontSize } // MARK: - Accessibility