Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-12-22 13:37:52 -06:00
parent 78f7cb1544
commit cac0af4ab3
3 changed files with 216 additions and 97 deletions

View File

@ -34,22 +34,9 @@
EAD890CE2EF1E9CF006DBA80 /* BaccaratUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BaccaratUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
EAD891062EF1F51E006DBA80 /* Exceptions for "Baccarat" folder in "Baccarat" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Agents.md,
);
target = EAD890B62EF1E9CE006DBA80 /* Baccarat */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EAD890B92EF1E9CE006DBA80 /* Baccarat */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
EAD891062EF1F51E006DBA80 /* Exceptions for "Baccarat" folder in "Baccarat" target */,
);
path = Baccarat;
sourceTree = "<group>";
};

View File

@ -29,15 +29,50 @@ enum Design {
// MARK: - Baccarat-Specific Component Sizes
enum Size {
// Cards - use CasinoDesign values
// MARK: - Hand Scaling
/// Hand scaling factor for cards and related elements.
/// 1.0 = original size, 1.5 = 50% larger, 2.0 = double size.
/// Adjust this value to change card sizes across the app.
static let handScale: CGFloat = 1.5
/// Scale multiplier for small screens (iPhone SE, etc).
/// Applied instead of handScale on screens narrower than smallScreenThreshold.
static let smallScreenScale: CGFloat = 1.2
/// Screen width threshold for small screen detection (iPhone SE is 375pt)
static let smallScreenThreshold: CGFloat = 390
/// Additional scale multiplier for large screens (iPad).
/// Applied on top of handScale when on regular size class.
static let largeScreenMultiplier: CGFloat = 1.2
// Cards - base values from CasinoDesign
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
static let cardWidthMedium: CGFloat = CasinoDesign.Size.cardWidthMedium
static let cardWidthLarge: CGFloat = CasinoDesign.Size.cardWidthLarge
static let cardAspectRatio: CGFloat = CasinoDesign.Size.cardAspectRatio
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap
// Baccarat table cards (smaller for compact layout)
static let cardWidthTable: CGFloat = 45
/// Base card width before scaling (for reference)
private static let cardWidthTableBase: CGFloat = 45
/// Card overlap scaled with hand size (standard iPhone)
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale
/// Card overlap for small screens
static let cardOverlapSmall: CGFloat = CasinoDesign.Size.cardOverlap * smallScreenScale
// Baccarat table cards - scaled for better visibility (standard iPhone)
static let cardWidthTable: CGFloat = cardWidthTableBase * handScale
/// Card width for small screens (iPhone SE, etc)
static let cardWidthTableSmall: CGFloat = cardWidthTableBase * smallScreenScale
/// Card width for large screens (iPad) - applies additional multiplier
static let cardWidthTableLarge: CGFloat = cardWidthTableBase * handScale * largeScreenMultiplier
/// Card overlap for large screens
static let cardOverlapLarge: CGFloat = CasinoDesign.Size.cardOverlap * handScale * largeScreenMultiplier
// Chips - use CasinoDesign values
static let chipSmall: CGFloat = CasinoDesign.Size.chipSmall

View File

@ -52,38 +52,40 @@ struct GameTableView: View {
}
var body: some View {
ZStack {
// Table background
TableBackgroundView()
GeometryReader { geometry in
ZStack {
// Table background
TableBackgroundView()
// Main content
VStack(spacing: 0) {
// Top bar with balance and info
TopBarView(
balance: state.balance,
cardsRemaining: state.engine.shoe.cardsRemaining,
showCardsRemaining: settings.showCardsRemaining,
onReset: { state.resetGame() },
onSettings: { showSettings = true },
onHelp: { showRules = true },
onStats: { showStats = true }
)
// Main content
VStack(spacing: 0) {
// Top bar with balance and info
TopBarView(
balance: state.balance,
cardsRemaining: state.engine.shoe.cardsRemaining,
showCardsRemaining: settings.showCardsRemaining,
onReset: { state.resetGame() },
onSettings: { showSettings = true },
onHelp: { showRules = true },
onStats: { showStats = true }
)
Spacer(minLength: Design.Spacing.xSmall)
Spacer(minLength: Design.Spacing.xSmall)
// Cards display area - constrained width on iPad
CardsDisplayArea(
playerCards: state.visiblePlayerCards,
bankerCards: state.visibleBankerCards,
playerCardsFaceUp: state.playerCardsFaceUp,
bankerCardsFaceUp: state.bankerCardsFaceUp,
playerValue: state.playerHandValue,
bankerValue: state.bankerHandValue,
playerIsWinner: playerIsWinner,
bankerIsWinner: bankerIsWinner,
isTie: isTie
)
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
// Cards display area - constrained width on iPad
CardsDisplayArea(
playerCards: state.visiblePlayerCards,
bankerCards: state.visibleBankerCards,
playerCardsFaceUp: state.playerCardsFaceUp,
bankerCardsFaceUp: state.bankerCardsFaceUp,
playerValue: state.playerHandValue,
bankerValue: state.bankerHandValue,
playerIsWinner: playerIsWinner,
bankerIsWinner: bankerIsWinner,
isTie: isTie,
screenWidth: geometry.size.width
)
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
Spacer(minLength: Design.Spacing.xSmall)
@ -134,37 +136,38 @@ struct GameTableView: View {
}
.safeAreaPadding(.bottom)
// Result banner overlay (handles its own iPad sizing)
if state.showResultBanner, let result = state.lastResult {
ResultBannerView(
result: result,
totalWinnings: state.lastWinnings,
betResults: state.betResults,
playerHadPair: state.playerHadPair,
bankerHadPair: state.bankerHadPair,
currentBalance: state.balance,
minBet: state.minBet,
onNewRound: { state.newRound() },
onGameOver: {
// Reset game (sound already played when banner appeared)
state.resetGame()
// Result banner overlay (handles its own iPad sizing)
if state.showResultBanner, let result = state.lastResult {
ResultBannerView(
result: result,
totalWinnings: state.lastWinnings,
betResults: state.betResults,
playerHadPair: state.playerHadPair,
bankerHadPair: state.bankerHadPair,
currentBalance: state.balance,
minBet: state.minBet,
onNewRound: { state.newRound() },
onGameOver: {
// Reset game (sound already played when banner appeared)
state.resetGame()
}
)
.transition(.opacity)
// Confetti for wins
if state.lastWinnings > 0 {
ConfettiView()
}
)
.transition(.opacity)
// Confetti for wins
if state.lastWinnings > 0 {
ConfettiView()
}
}
// Game Over overlay (handles its own iPad sizing)
if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating {
GameOverView(
roundsPlayed: state.roundHistory.count,
onPlayAgain: { state.resetGame() }
)
.transition(.opacity)
// Game Over overlay (handles its own iPad sizing)
if state.balance == 0 && state.currentBets.isEmpty && !state.isAnimating {
GameOverView(
roundsPlayed: state.roundHistory.count,
onPlayAgain: { state.resetGame() }
)
.transition(.opacity)
}
}
}
.onAppear {
@ -347,11 +350,27 @@ struct CardsDisplayArea: View {
let playerIsWinner: Bool
let bankerIsWinner: Bool
let isTie: Bool
/// Screen width for responsive card sizing
var screenWidth: CGFloat = 400
// MARK: - Fixed font sizes for card area
// Fixed because the card display has strict layout constraints
// MARK: - Environment
private let labelFontSize: CGFloat = 14
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
// MARK: - Scaled font sizes for card area
// Scales with hand size for proportional appearance
/// Whether we're on a large screen (iPad)
private var isLargeScreen: Bool {
horizontalSizeClass == .regular
}
/// Label font size - only scales on iPad to avoid clipping on small iPhones
private var labelFontSize: CGFloat {
let baseSize: CGFloat = 14
// Only apply scaling on large screens; keep original size on iPhone
return isLargeScreen ? baseSize * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseSize
}
// MARK: - Accessibility
@ -387,8 +406,25 @@ struct CardsDisplayArea: View {
return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue)
}
/// Minimum height for label row - only scales on iPad
private var labelRowMinHeight: CGFloat {
let baseHeight: CGFloat = 30
// Only apply scaling on large screens; keep original size on iPhone
return isLargeScreen ? baseHeight * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseHeight
}
/// Spacing between PLAYER and BANKER hands - reduced on smaller screens
private var handsSpacing: CGFloat {
isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.medium
}
/// Horizontal padding inside the container - reduced on smaller screens
private var containerPaddingH: CGFloat {
isLargeScreen ? Design.Spacing.xLarge : Design.Spacing.small
}
var body: some View {
HStack(spacing: Design.Spacing.xxxLarge) {
HStack(spacing: handsSpacing) {
// Player side
VStack(spacing: Design.Spacing.small) {
// Label with value
@ -401,13 +437,14 @@ struct CardsDisplayArea: View {
ValueBadge(value: playerValue, color: .blue)
}
}
.frame(minHeight: 30)
.frame(minHeight: labelRowMinHeight)
// Cards
CompactHandView(
cards: playerCards,
cardsFaceUp: playerCardsFaceUp,
isWinner: playerIsWinner
isWinner: playerIsWinner,
screenWidth: screenWidth
)
}
.accessibilityElement(children: .ignore)
@ -426,28 +463,29 @@ struct CardsDisplayArea: View {
ValueBadge(value: bankerValue, color: .red)
}
}
.frame(minHeight: 30)
.frame(minHeight: labelRowMinHeight)
// Cards
CompactHandView(
cards: bankerCards,
cardsFaceUp: bankerCardsFaceUp,
isWinner: bankerIsWinner
isWinner: bankerIsWinner,
screenWidth: screenWidth
)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Banker hand"))
.accessibilityValue(bankerHandDescription + (bankerIsWinner ? ", " + String(localized: "Winner") : ""))
}
.padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.xLarge)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.top, Design.Spacing.medium)
.padding(.bottom, Design.Spacing.large)
.padding(.horizontal, containerPaddingH)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.black.opacity(Design.Opacity.quarter))
.accessibilityHidden(true)
)
.padding(.horizontal)
.padding(.horizontal, Design.Spacing.small)
}
}
@ -456,17 +494,59 @@ struct CompactHandView: View {
let cards: [Card]
let cardsFaceUp: [Bool]
let isWinner: Bool
/// Screen width passed from parent for responsive sizing
var screenWidth: CGFloat = 400
// MARK: - Scaled Font Sizes (Dynamic Type)
// MARK: - Environment
@ScaledMetric(relativeTo: .caption) private var winBadgeFontSize: CGFloat = 10
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
// MARK: - Layout Constants
// Fixed size: cards have strict visual constraints
// Responsive sizing based on device
/// Whether we're on a large screen (iPad)
private var isLargeScreen: Bool {
horizontalSizeClass == .regular
}
/// Whether we're on a small screen (iPhone SE, etc)
private var isSmallScreen: Bool {
!isLargeScreen && screenWidth < Design.Size.smallScreenThreshold
}
/// WIN badge font size - only scales on iPad
private var winBadgeFontSize: CGFloat {
let baseSize: CGFloat = 10
return isLargeScreen ? baseSize * Design.Size.handScale * Design.Size.largeScreenMultiplier : baseSize
}
/// Card width - responsive based on screen size
private var cardWidth: CGFloat {
if isLargeScreen {
return Design.Size.cardWidthTableLarge
} else if isSmallScreen {
return Design.Size.cardWidthTableSmall
} else {
return Design.Size.cardWidthTable
}
}
/// Card height based on aspect ratio
private var cardHeight: CGFloat {
cardWidth * Design.Size.cardAspectRatio
}
/// Card overlap - scaled with card size
private var cardOverlap: CGFloat {
if isLargeScreen {
return Design.Size.cardOverlapLarge
} else if isSmallScreen {
return Design.Size.cardOverlapSmall
} else {
return Design.Size.cardOverlap
}
}
private let cardWidth: CGFloat = Design.Size.cardWidthTable
private let cardHeight: CGFloat = Design.Size.cardWidthTable * Design.Size.cardAspectRatio
private let cardOverlap: CGFloat = Design.Size.cardOverlap
private let placeholderSpacing: CGFloat = Design.Spacing.small
/// Fixed container width to prevent resizing during deal
@ -538,10 +618,27 @@ struct ValueBadge: View {
let value: Int
let color: Color
// MARK: - Environment
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
// MARK: - Scaled Font Sizes (Dynamic Type)
@ScaledMetric(relativeTo: .headline) private var valueFontSize: CGFloat = 15
@ScaledMetric(relativeTo: .headline) private var badgeSize: CGFloat = 26
/// Whether we're on a large screen (iPad)
private var isLargeScreen: Bool {
horizontalSizeClass == .regular
}
/// Scale factor for badge sizing - only applies on iPad to avoid clipping on iPhone
private var scale: CGFloat {
isLargeScreen ? Design.Size.handScale * Design.Size.largeScreenMultiplier : 1.0
}
@ScaledMetric(relativeTo: .headline) private var baseValueFontSize: CGFloat = 15
@ScaledMetric(relativeTo: .headline) private var baseBadgeSize: CGFloat = 26
private var valueFontSize: CGFloat { baseValueFontSize * scale }
private var badgeSize: CGFloat { baseBadgeSize * scale }
var body: some View {
Text("\(value)")