Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
a9b4f95bb4
commit
982d54ed1d
@ -349,7 +349,7 @@ final class GameState {
|
||||
syncSoundSettings()
|
||||
loadSavedGame()
|
||||
|
||||
// Start timing for the first round's betting phase
|
||||
// Start timing for the first round (includes betting phase)
|
||||
roundStartTime = Date()
|
||||
}
|
||||
|
||||
@ -503,9 +503,9 @@ final class GameState {
|
||||
func deal() async {
|
||||
guard canDeal else { return }
|
||||
|
||||
// Track bet amount for statistics (roundStartTime is set in newRound)
|
||||
// Track bet amount for statistics (roundStartTime was set when betting phase started)
|
||||
roundBetAmount = currentBet + perfectPairsBet + twentyOnePlusThreeBet
|
||||
Design.debugLog("🎴 Deal started - roundStartTime: \(String(describing: roundStartTime)), roundBetAmount: \(roundBetAmount)")
|
||||
Design.debugLog("🎴 Deal started - roundBetAmount: \(roundBetAmount), time since round start: \(Date().timeIntervalSince(roundStartTime ?? Date()))s")
|
||||
|
||||
// Ensure enough cards for a full hand - reshuffle if needed
|
||||
if !engine.canDealNewHand {
|
||||
@ -1224,15 +1224,15 @@ final class GameState {
|
||||
twentyOnePlusThreeResult = nil
|
||||
showSideBetToasts = false
|
||||
|
||||
// Start timing for the new round (includes betting phase)
|
||||
roundStartTime = Date()
|
||||
Design.debugLog("🎰 New round started - roundStartTime: \(roundStartTime!)")
|
||||
|
||||
// Reset UI state
|
||||
showResultBanner = false
|
||||
lastRoundResult = nil
|
||||
currentPhase = .betting
|
||||
|
||||
// Start timing for the new round (includes betting phase)
|
||||
roundStartTime = Date()
|
||||
Design.debugLog("🎰 New round started - roundStartTime: \(roundStartTime!)")
|
||||
|
||||
sound.play(.newRound)
|
||||
}
|
||||
|
||||
|
||||
@ -5068,6 +5068,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"PLAYER" : {
|
||||
"comment" : "Title to display for a player hand when the hand number is not available.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Player hand: %@. Value: %@" : {
|
||||
"comment" : "A user-readable string describing a player's blackjack hand, including the card values and any relevant game results. The argument is a comma-separated list of the card descriptions in the player's hand.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
|
||||
@ -156,7 +156,8 @@ extension Color {
|
||||
enum Hand {
|
||||
static let player = Color(red: 0.2, green: 0.5, blue: 0.8)
|
||||
static let dealer = Color(red: 0.8, green: 0.3, blue: 0.3)
|
||||
static let active = Color.yellow
|
||||
/// Bright emerald green - readable with white text, indicates "your turn"
|
||||
static let active = Color(red: 0.0, green: 0.7, blue: 0.45)
|
||||
static let inactive = Color.white.opacity(CasinoDesign.Opacity.medium)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// DealerHandView.swift
|
||||
// Blackjack
|
||||
//
|
||||
// Displays the dealer's hand with cards and value.
|
||||
// Displays the dealer's hand with label, value badge, and cards.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@ -21,37 +21,33 @@ struct DealerHandView: View {
|
||||
let visibleCardCount: Int
|
||||
|
||||
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
|
||||
@ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge
|
||||
|
||||
/// The value to display in the badge (based on visible cards and hole card state).
|
||||
private var displayValue: Int? {
|
||||
guard !hand.cards.isEmpty && visibleCardCount > 0 else { return nil }
|
||||
|
||||
if showHoleCard {
|
||||
// Hole card revealed - calculate value from visible cards
|
||||
let visibleCards = Array(hand.cards.prefix(visibleCardCount))
|
||||
return BlackjackHand.bestValue(for: visibleCards)
|
||||
} else {
|
||||
// Hole card hidden - show only the first (face-up) card's value
|
||||
return hand.cards[0].blackjackValue
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
// Label and value - fixed height prevents vertical layout shift
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Text(String(localized: "DEALER"))
|
||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
// Calculate value from visible cards only
|
||||
if !hand.cards.isEmpty && visibleCardCount > 0 {
|
||||
if showHoleCard {
|
||||
// Hole card revealed - calculate value from visible cards
|
||||
let visibleCards = Array(hand.cards.prefix(visibleCardCount))
|
||||
let displayValue = BlackjackHand.bestValue(for: visibleCards)
|
||||
ValueBadge(value: displayValue, color: Color.Hand.dealer)
|
||||
.animation(nil, value: displayValue) // No animation when value changes
|
||||
} else {
|
||||
// Hole card hidden - show only the first (face-up) card's value
|
||||
ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer)
|
||||
.animation(nil, value: hand.cards[0].blackjackValue) // No animation when value changes
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minHeight: badgeHeight) // Reserve consistent height
|
||||
// Remove animations on badge appearance/value changes
|
||||
// Label and value badge
|
||||
HandLabelView(
|
||||
title: String(localized: "DEALER"),
|
||||
value: displayValue,
|
||||
badgeColor: Color.Hand.dealer
|
||||
)
|
||||
.animation(nil, value: visibleCardCount)
|
||||
.animation(nil, value: showHoleCard)
|
||||
// Cards with result badge overlay (overlay prevents height change)
|
||||
|
||||
// Cards with result badge overlay
|
||||
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
||||
CardStackView.dealer(
|
||||
cards: hand.cards,
|
||||
@ -118,6 +114,8 @@ struct DealerHandView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Empty Hand") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
@ -171,4 +169,3 @@ struct DealerHandView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
107
Blackjack/Blackjack/Views/Table/HandLabelView.swift
Normal file
107
Blackjack/Blackjack/Views/Table/HandLabelView.swift
Normal file
@ -0,0 +1,107 @@
|
||||
//
|
||||
// HandLabelView.swift
|
||||
// Blackjack
|
||||
//
|
||||
// Shared label component for hand displays (dealer and player).
|
||||
// Shows title text with an optional value badge.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CasinoKit
|
||||
|
||||
/// Displays a hand label with title and value badge.
|
||||
/// Used by both DealerHandView and PlayerHandView for consistent appearance.
|
||||
struct HandLabelView: View {
|
||||
let title: String
|
||||
let value: Int?
|
||||
let valueText: String?
|
||||
let badgeColor: Color
|
||||
|
||||
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
|
||||
@ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge
|
||||
|
||||
/// Creates a hand label with a numeric value badge.
|
||||
init(title: String, value: Int?, badgeColor: Color) {
|
||||
self.title = title
|
||||
self.value = value
|
||||
self.valueText = nil
|
||||
self.badgeColor = badgeColor
|
||||
}
|
||||
|
||||
/// Creates a hand label with a text value badge (for soft hands like "8/18").
|
||||
init(title: String, valueText: String?, badgeColor: Color) {
|
||||
self.title = title
|
||||
self.value = nil
|
||||
self.valueText = valueText
|
||||
self.badgeColor = badgeColor
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Text(title)
|
||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
if let value = value {
|
||||
ValueBadge(value: value, color: badgeColor)
|
||||
} else if let text = valueText, !text.isEmpty {
|
||||
TextValueBadge(text: text, color: badgeColor)
|
||||
}
|
||||
}
|
||||
.fixedSize() // Prevent the label from being constrained/truncated
|
||||
.frame(minHeight: badgeHeight)
|
||||
.animation(nil, value: value)
|
||||
.animation(nil, value: valueText)
|
||||
}
|
||||
}
|
||||
|
||||
/// A badge that displays text (for soft hand values like "8/18").
|
||||
struct TextValueBadge: View {
|
||||
let text: String
|
||||
let color: Color
|
||||
|
||||
// Match ValueBadge styling from CasinoKit
|
||||
@ScaledMetric(relativeTo: .headline) private var fontSize: CGFloat = 15
|
||||
@ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge
|
||||
@ScaledMetric(relativeTo: .headline) private var badgePadding: CGFloat = 8
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.font(.system(size: fontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
.fixedSize(horizontal: true, vertical: false) // Prevent truncation
|
||||
.padding(.horizontal, badgePadding)
|
||||
.frame(minHeight: badgeHeight)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(color)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Dealer Label") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
VStack(spacing: Design.Spacing.large) {
|
||||
HandLabelView(title: "DEALER", value: 21, badgeColor: Color.Hand.dealer)
|
||||
HandLabelView(title: "DEALER", value: 17, badgeColor: Color.Hand.dealer)
|
||||
HandLabelView(title: "DEALER", value: nil, badgeColor: Color.Hand.dealer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Player Label") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
VStack(spacing: Design.Spacing.large) {
|
||||
HandLabelView(title: "PLAYER", value: 21, badgeColor: Color.Hand.player)
|
||||
HandLabelView(title: "PLAYER", valueText: "8/18", badgeColor: Color.Hand.player)
|
||||
HandLabelView(title: "Hand 1", value: 17, badgeColor: Color.Hand.player)
|
||||
HandLabelView(title: "Hand 2", valueText: "5/15", badgeColor: Color.Hand.active)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// PlayerHandView.swift
|
||||
// Blackjack
|
||||
//
|
||||
// Displays a single player hand with cards, value, bet, and result.
|
||||
// Displays a single player hand with label, value badge, cards, and bet.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@ -34,8 +34,21 @@ struct PlayerHandView: View {
|
||||
@ScaledMetric(relativeTo: .body) private var hintPaddingH: CGFloat = Design.Size.hintPaddingH
|
||||
@ScaledMetric(relativeTo: .body) private var hintPaddingV: CGFloat = Design.Size.hintPaddingV
|
||||
|
||||
/// The title to display (PLAYER or Hand #).
|
||||
private var displayTitle: String {
|
||||
if let number = handNumber {
|
||||
return String(localized: "Hand \(number)")
|
||||
}
|
||||
return String(localized: "PLAYER")
|
||||
}
|
||||
|
||||
/// The badge color (active hands get highlight color).
|
||||
private var badgeColor: Color {
|
||||
isActive ? Color.Hand.active : Color.Hand.player
|
||||
}
|
||||
|
||||
/// Calculates display info for visible cards using shared BlackjackHand logic.
|
||||
private var visibleCardsDisplayInfo: (text: String, color: Color)? {
|
||||
private var visibleCardsDisplayInfo: (text: String, value: Int, isBusted: Bool)? {
|
||||
guard !hand.cards.isEmpty else { return nil }
|
||||
|
||||
let visibleCards = Array(hand.cards.prefix(visibleCardCount))
|
||||
@ -45,27 +58,24 @@ struct PlayerHandView: View {
|
||||
let (hardValue, softValue) = BlackjackHand.calculateValues(for: visibleCards)
|
||||
let displayValue = BlackjackHand.bestValue(for: visibleCards)
|
||||
let hasSoftAce = BlackjackHand.hasSoftAce(for: visibleCards)
|
||||
|
||||
// Determine color based on visible cards
|
||||
let isVisibleBlackjack = visibleCards.count == 2 && displayValue == 21 && !hand.isSplit
|
||||
let isVisibleBusted = hardValue > 21
|
||||
let isVisible21 = displayValue == 21 && !isVisibleBlackjack
|
||||
|
||||
let displayColor: Color = {
|
||||
if isVisibleBlackjack { return .yellow }
|
||||
if isVisibleBusted { return .red }
|
||||
if isVisible21 { return .green }
|
||||
return .white
|
||||
}()
|
||||
let isBusted = hardValue > 21
|
||||
|
||||
// Show value like hand.valueDisplay does (e.g., "8/18" for soft hands)
|
||||
let valueText = hasSoftAce ? "\(hardValue)/\(softValue)" : "\(displayValue)"
|
||||
|
||||
return (valueText, displayColor)
|
||||
return (valueText, displayValue, isBusted)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
// Label and value badge at TOP (consistent with dealer)
|
||||
HandLabelView(
|
||||
title: displayTitle,
|
||||
valueText: visibleCardsDisplayInfo?.text,
|
||||
badgeColor: badgeColor
|
||||
)
|
||||
.animation(nil, value: visibleCardCount)
|
||||
|
||||
// Cards with container
|
||||
CardStackView.player(
|
||||
cards: hand.cards,
|
||||
@ -112,39 +122,25 @@ struct PlayerHandView: View {
|
||||
.animation(.easeInOut(duration: Design.Animation.quick), value: isActive)
|
||||
.animation(.spring(duration: Design.Animation.springDuration), value: hand.result != nil)
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// Calculate value from visible (animation-completed) cards only
|
||||
if let displayInfo = visibleCardsDisplayInfo {
|
||||
Text(displayInfo.text)
|
||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(displayInfo.color)
|
||||
.animation(nil, value: displayInfo.text)
|
||||
.animation(nil, value: displayInfo.color)
|
||||
}
|
||||
|
||||
if hand.isDoubledDown {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(.purple)
|
||||
}
|
||||
}
|
||||
|
||||
// Bet amount
|
||||
if hand.bet > 0 {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "dollarsign.circle.fill")
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(.yellow)
|
||||
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
|
||||
.font(.system(size: handNumberSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.yellow)
|
||||
// Bet amount and doubled down indicator
|
||||
if hand.bet > 0 || hand.isDoubledDown {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
if hand.bet > 0 {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "dollarsign.circle.fill")
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(.yellow)
|
||||
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
|
||||
.font(.system(size: handNumberSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
}
|
||||
|
||||
if hand.isDoubledDown {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(.purple)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,13 +150,6 @@ struct PlayerHandView: View {
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
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)")
|
||||
@ -192,7 +181,7 @@ struct PlayerHandView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("With Cards") {
|
||||
#Preview("With Cards - Active") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
PlayerHandView(
|
||||
@ -214,15 +203,15 @@ struct PlayerHandView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Blackjack") {
|
||||
#Preview("Soft Hand") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
PlayerHandView(
|
||||
hand: BlackjackHand(cards: [
|
||||
Card(suit: .hearts, rank: .ace),
|
||||
Card(suit: .spades, rank: .king)
|
||||
Card(suit: .spades, rank: .seven)
|
||||
], bet: 100),
|
||||
isActive: false,
|
||||
isActive: true,
|
||||
showCardCount: true,
|
||||
showAnimations: true,
|
||||
dealingSpeed: 1.0,
|
||||
@ -236,7 +225,7 @@ struct PlayerHandView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Split Hand") {
|
||||
#Preview("Split Hand - Inactive") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
PlayerHandView(
|
||||
@ -244,7 +233,7 @@ struct PlayerHandView: View {
|
||||
Card(suit: .clubs, rank: .eight),
|
||||
Card(suit: .spades, rank: .jack)
|
||||
], bet: 100),
|
||||
isActive: true,
|
||||
isActive: false,
|
||||
showCardCount: false,
|
||||
showAnimations: true,
|
||||
dealingSpeed: 1.0,
|
||||
@ -252,8 +241,8 @@ struct PlayerHandView: View {
|
||||
cardWidth: 60,
|
||||
cardSpacing: -20,
|
||||
visibleCardCount: 2,
|
||||
currentHint: "Stand",
|
||||
showHintToast: true
|
||||
currentHint: nil,
|
||||
showHintToast: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user