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

This commit is contained in:
Matt Bruce 2025-12-28 18:53:52 -06:00
parent fa016047c2
commit e8e0a68c24
3 changed files with 97 additions and 66 deletions

View File

@ -610,8 +610,14 @@ final class GameState {
bankerHadPair = false
betResults = []
// Deal initial cards
// Change to dealing phase - triggers layout animation (horizontal to vertical)
currentPhase = .dealingInitial
// Wait for layout animation to complete before dealing cards
if settings.showAnimations {
try? await Task.sleep(for: .seconds(1))
}
let initialCards = engine.dealInitialCards()
// Check if animations are enabled

View File

@ -17,6 +17,9 @@ struct GameTableView: View {
@State private var showRules = false
@State private var showStats = false
/// Screen size for card sizing (measured from TableBackgroundView)
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
@ -76,8 +79,13 @@ struct GameTableView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
// Table background (from CasinoKit)
// Table background - measures screen size for card sizing
TableBackgroundView()
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { size in
screenSize = size
}
// Main content
mainContent(geometry: geometry)
@ -192,7 +200,8 @@ struct GameTableView: View {
showAnimations: settings.showAnimations,
dealingSpeed: settings.dealingSpeed,
bettedOnPlayer: state.bettedOnPlayer,
isDealing: isDealing
isDealing: isDealing,
screenSize: screenSize
)
.frame(maxWidth: maxContentWidth)
.padding(.horizontal, Design.Spacing.medium)
@ -289,7 +298,8 @@ struct GameTableView: View {
showAnimations: settings.showAnimations,
dealingSpeed: settings.dealingSpeed,
bettedOnPlayer: state.bettedOnPlayer,
isDealing: isDealing
isDealing: isDealing,
screenSize: screenSize
)
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
.padding(.horizontal, Design.Spacing.medium)

View File

@ -3,8 +3,7 @@
// Baccarat
//
// The cards display area showing both Player and Banker hands.
// Defaults to side-by-side during betting, animates to vertical during dealing
// with the betted hand on bottom.
// Animates from side-by-side (betting) to vertical stack (dealing).
//
import SwiftUI
@ -12,7 +11,7 @@ import CasinoKit
/// The cards display area showing both hands.
/// - Betting phase: Horizontal side-by-side layout (Player | Banker)
/// - Dealing/Result phases: Vertical layout with betted hand on bottom
/// - Dealing phase: Vertical stack with betted hand on bottom
struct CardsDisplayArea: View {
let playerCards: [Card]
let bankerCards: [Card]
@ -26,39 +25,48 @@ struct CardsDisplayArea: View {
let showAnimations: Bool
let dealingSpeed: Double
/// Which main bet is placed - nil if no main bet, true if Player, false if Banker.
/// Determines layout ordering in vertical mode: betted hand appears on bottom.
let bettedOnPlayer: Bool?
/// Whether the game is in dealing/result phase (vertical layout) or betting phase (horizontal).
let isDealing: Bool
/// Full screen size for calculating card width in dealing mode.
let screenSize: CGSize
// MARK: - State
@State private var containerWidth: CGFloat = 300
@Namespace private var animation
// MARK: - Environment
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
// MARK: - Computed Properties
/// Whether we're on a large screen (iPad)
private var isLargeScreen: Bool {
horizontalSizeClass == .regular
}
// Use global debug flag from Design constants
/// Whether we're in landscape mode
private var isLandscape: Bool {
verticalSizeClass == .compact || (isLargeScreen && screenSize.width > screenSize.height)
}
private var showDebugBorders: Bool { Design.showDebugBorders }
/// Label font size - larger on iPad
private var labelFontSize: CGFloat {
isLargeScreen ? 18 : Design.Size.labelFontSize
}
/// Minimum height for label row - larger on iPad
private var labelRowMinHeight: CGFloat {
isLargeScreen ? 40 : Design.Size.labelRowHeight
}
/// Whether Player hand should be on bottom in vertical mode.
private var playerOnBottom: Bool {
bettedOnPlayer ?? true
}
/// Spacing between hands
private var handsSpacing: CGFloat {
if isDealing {
@ -68,13 +76,35 @@ struct CardsDisplayArea: View {
}
}
/// Card section width - larger in vertical mode (more horizontal space)
// MARK: - Card Width Calculations
/// Card section width for horizontal (betting) mode
private var horizontalHandWidth: CGFloat {
let spacing = isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large
return max(100, (containerWidth - spacing) / 2)
}
/// Card section width for vertical (dealing) mode - matches Blackjack sizing
/// Uses screen height as base (like Blackjack), which is smaller in landscape
private var verticalHandWidth: CGFloat {
// Use screen height (smaller dimension in landscape = smaller cards)
let height = screenSize.height
guard height > 100 else { return horizontalHandWidth }
// Blackjack uses 0.18, but in landscape we may need slightly smaller
let percentage: CGFloat = isLandscape ? 0.14 : 0.18
let cardWidth = height * percentage
// CompactHandView: cardWidth = containerWidth / divisor
let overlapRatio: CGFloat = -0.45
let maxCards: CGFloat = 3
let divisor = 1 + (maxCards - 1) * (1 + overlapRatio)
return cardWidth * divisor
}
/// Current hand section width based on mode
private var handSectionWidth: CGFloat {
if isDealing {
return containerWidth * 0.7
} else {
return (containerWidth - handsSpacing) / 2
}
isDealing ? verticalHandWidth : horizontalHandWidth
}
// MARK: - Accessibility
@ -111,49 +141,35 @@ struct CardsDisplayArea: View {
return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue)
}
/// Whether Player hand should be on bottom (true) or top (false) in vertical mode.
/// Defaults to Player on bottom if no bet placed.
private var playerOnBottom: Bool {
bettedOnPlayer ?? true
}
/// The layout to use - HStack for horizontal, VStack for vertical
private var layout: AnyLayout {
isDealing ? AnyLayout(VStackLayout(spacing: handsSpacing)) : AnyLayout(HStackLayout(spacing: handsSpacing))
}
// MARK: - Body
var body: some View {
layout {
// First position: Player in horizontal, or top hand in vertical
if isDealing && !playerOnBottom {
// Vertical mode, player on top
playerHandSection(width: handSectionWidth)
.debugBorder(showDebugBorders, color: .blue, label: "Player")
} else if isDealing && playerOnBottom {
// Vertical mode, banker on top
bankerHandSection(width: handSectionWidth)
.debugBorder(showDebugBorders, color: .red, label: "Banker")
// Use different layouts but keep view identity with matchedGeometryEffect
Group {
if isDealing {
// Vertical layout
VStack(spacing: handsSpacing) {
// Top position
if playerOnBottom {
bankerHandSection(width: handSectionWidth)
.matchedGeometryEffect(id: "banker", in: animation)
playerHandSection(width: handSectionWidth)
.matchedGeometryEffect(id: "player", in: animation)
} else {
playerHandSection(width: handSectionWidth)
.matchedGeometryEffect(id: "player", in: animation)
bankerHandSection(width: handSectionWidth)
.matchedGeometryEffect(id: "banker", in: animation)
}
}
} else {
// Horizontal mode - player always on left
playerHandSection(width: handSectionWidth)
.debugBorder(showDebugBorders, color: .blue, label: "Player")
}
// Second position: Banker in horizontal, or bottom hand in vertical
if isDealing && !playerOnBottom {
// Vertical mode, banker on bottom
bankerHandSection(width: handSectionWidth)
.debugBorder(showDebugBorders, color: .red, label: "Banker")
} else if isDealing && playerOnBottom {
// Vertical mode, player on bottom
playerHandSection(width: handSectionWidth)
.debugBorder(showDebugBorders, color: .blue, label: "Player")
} else {
// Horizontal mode - banker always on right
bankerHandSection(width: handSectionWidth)
.debugBorder(showDebugBorders, color: .red, label: "Banker")
// Horizontal layout - Player left, Banker right
HStack(spacing: handsSpacing) {
playerHandSection(width: handSectionWidth)
.matchedGeometryEffect(id: "player", in: animation)
bankerHandSection(width: handSectionWidth)
.matchedGeometryEffect(id: "banker", in: animation)
}
}
}
.frame(maxWidth: .infinity)
@ -176,15 +192,14 @@ struct CardsDisplayArea: View {
.accessibilityHidden(true)
)
.debugBorder(showDebugBorders, color: .mint, label: "HandsContainer")
.animation(.spring(duration: 0.5, bounce: 0.2), value: isDealing)
.animation(.spring(duration: 0.4, bounce: 0.15), value: playerOnBottom)
.animation(.spring(duration: 0.6, bounce: 0.2), value: isDealing)
.animation(.spring(duration: 0.5, bounce: 0.15), value: playerOnBottom)
}
// MARK: - Private Views
private func playerHandSection(width: CGFloat) -> some View {
VStack(spacing: Design.Spacing.small) {
// Label with value
HStack(spacing: Design.Spacing.small) {
Text("PLAYER")
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
@ -196,7 +211,6 @@ struct CardsDisplayArea: View {
}
.frame(minHeight: labelRowMinHeight)
// Cards
CompactHandView(
cards: playerCards,
cardsFaceUp: playerCardsFaceUp,
@ -214,7 +228,6 @@ struct CardsDisplayArea: View {
private func bankerHandSection(width: CGFloat) -> some View {
VStack(spacing: Design.Spacing.small) {
// Label with value
HStack(spacing: Design.Spacing.small) {
Text("BANKER")
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
@ -226,7 +239,6 @@ struct CardsDisplayArea: View {
}
.frame(minHeight: labelRowMinHeight)
// Cards
CompactHandView(
cards: bankerCards,
cardsFaceUp: bankerCardsFaceUp,
@ -261,7 +273,8 @@ struct CardsDisplayArea: View {
showAnimations: true,
dealingSpeed: 1.0,
bettedOnPlayer: nil,
isDealing: false
isDealing: false,
screenSize: CGSize(width: 400, height: 800)
)
}
}
@ -288,7 +301,8 @@ struct CardsDisplayArea: View {
showAnimations: true,
dealingSpeed: 1.0,
bettedOnPlayer: true,
isDealing: true
isDealing: true,
screenSize: CGSize(width: 400, height: 800)
)
}
}
@ -317,7 +331,8 @@ struct CardsDisplayArea: View {
showAnimations: true,
dealingSpeed: 1.0,
bettedOnPlayer: false,
isDealing: true
isDealing: true,
screenSize: CGSize(width: 400, height: 800)
)
}
}