diff --git a/Baccarat/Baccarat/Engine/GameState.swift b/Baccarat/Baccarat/Engine/GameState.swift index 65a00ed..d783f1b 100644 --- a/Baccarat/Baccarat/Engine/GameState.swift +++ b/Baccarat/Baccarat/Engine/GameState.swift @@ -45,12 +45,14 @@ struct BetResult: Identifiable { } /// Main observable game state class managing all game logic and UI state. +/// Conforms to CasinoGameState for shared game behaviors. @Observable @MainActor -final class GameState: SessionManagedGame { - // MARK: - SessionManagedGame +final class GameState: CasinoGameState { + // MARK: - CasinoGameState Conformance typealias Stats = BaccaratStats + typealias GameSettingsType = GameSettings /// The currently active session. var currentSession: BaccaratSession? diff --git a/Baccarat/Baccarat/Models/Card+Baccarat.swift b/Baccarat/Baccarat/Models/Card+Baccarat.swift new file mode 100644 index 0000000..058cfd8 --- /dev/null +++ b/Baccarat/Baccarat/Models/Card+Baccarat.swift @@ -0,0 +1,30 @@ +// +// Card+Baccarat.swift +// Baccarat +// +// Baccarat-specific card value extensions. +// + +import CasinoKit + +extension Rank { + /// The baccarat point value of this rank. + /// Ace = 1, 2-9 = face value, 10/J/Q/K = 0. + var baccaratValue: Int { + switch self { + case .ace: return 1 + case .two, .three, .four, .five, .six, .seven, .eight, .nine: + return rawValue + case .ten, .jack, .queen, .king: + return 0 + } + } +} + +extension Card { + /// The baccarat point value of this card. + var baccaratValue: Int { + rank.baccaratValue + } +} + diff --git a/Baccarat/Baccarat/Models/GameSettings.swift b/Baccarat/Baccarat/Models/GameSettings.swift index e3d2bd3..6be80d1 100644 --- a/Baccarat/Baccarat/Models/GameSettings.swift +++ b/Baccarat/Baccarat/Models/GameSettings.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import CasinoKit /// The number of decks available for the shoe. enum DeckCount: Int, CaseIterable, Identifiable { @@ -33,65 +34,13 @@ enum DeckCount: Int, CaseIterable, Identifiable { } } -/// Preset table limits for betting. -enum TableLimits: String, CaseIterable, Identifiable { - case casual = "casual" - case low = "low" - case medium = "medium" - case high = "high" - case vip = "vip" - - var id: String { rawValue } - - var displayName: String { - switch self { - case .casual: return "Casual" - case .low: return "Low Stakes" - case .medium: return "Medium Stakes" - case .high: return "High Stakes" - case .vip: return "VIP" - } - } - - var minBet: Int { - switch self { - case .casual: return 5 - case .low: return 10 - case .medium: return 25 - case .high: return 100 - case .vip: return 500 - } - } - - var maxBet: Int { - switch self { - case .casual: return 500 - case .low: return 1_000 - case .medium: return 5_000 - case .high: return 10_000 - case .vip: return 50_000 - } - } - - var description: String { - "$\(minBet) - $\(maxBet.formatted())" - } - - var detailedDescription: String { - switch self { - case .casual: return "Perfect for learning" - case .low: return "Standard mini baccarat" - case .medium: return "Regular casino table" - case .high: return "High roller table" - case .vip: return "Exclusive VIP room" - } - } -} +// TableLimits is now provided by CasinoKit -/// Observable settings class for game configuration. +/// Observable settings class for Baccarat configuration. +/// Conforms to GameSettingsProtocol for shared settings behavior. @Observable @MainActor -final class GameSettings { +final class GameSettings: GameSettingsProtocol { // MARK: - Deck Settings /// Number of decks in the shoe. diff --git a/Baccarat/Baccarat/Resources/Localizable.xcstrings b/Baccarat/Baccarat/Resources/Localizable.xcstrings index 829a53e..92cfed2 100644 --- a/Baccarat/Baccarat/Resources/Localizable.xcstrings +++ b/Baccarat/Baccarat/Resources/Localizable.xcstrings @@ -354,9 +354,6 @@ } } } - }, - "$%lld" : { - }, "2-9: Face value" : { "comment" : "Description of the card values for cards with values from 2 to 9.", @@ -684,10 +681,6 @@ } } }, - "Average bet" : { - "comment" : "The value of this row is calculated as the total bet amount divided by the number of rounds played.", - "isCommentAutoGenerated" : true - }, "Avoid the Tie bet — 14.4% house edge!" : { "comment" : "Tip for avoiding the Tie bet in baccarat, highlighting its low house edge.", "localizations" : { @@ -1031,14 +1024,6 @@ "comment" : "Label for the number of banker win rounds in the statistics display.", "isCommentAutoGenerated" : true }, - "Best gain" : { - "comment" : "\"Best gain\" is a colloquial term for the largest positive winnings in a single game.", - "isCommentAutoGenerated" : true - }, - "Best session" : { - "comment" : "A label for the best session amount in the statistics sheet.", - "isCommentAutoGenerated" : true - }, "Bet on Player, Banker, or Tie" : { }, @@ -1072,10 +1057,6 @@ "comment" : "Title for the section in the statistics sheet that shows the user's performance on the Big Road.", "isCommentAutoGenerated" : true }, - "Biggest bet" : { - "comment" : "The label for the \"Biggest bet\" row in the statistics sheet.", - "isCommentAutoGenerated" : true - }, "Blackjack" : { "comment" : "The name of a blackjack game.", "localizations" : { @@ -1382,13 +1363,6 @@ } } } - }, - "Completed sessions will appear here." : { - "comment" : "A description below the label indicating that completed sessions will be displayed here.", - "isCommentAutoGenerated" : true - }, - "Current" : { - }, "Customize Settings" : { @@ -1489,9 +1463,6 @@ "Delete" : { "comment" : "A button to delete a session.", "isCommentAutoGenerated" : true - }, - "Delete Session" : { - }, "Delete Session?" : { @@ -1644,13 +1615,6 @@ "End Session?" : { "comment" : "A confirmation dialog title.", "isCommentAutoGenerated" : true - }, - "Ended manually" : { - "comment" : "A description of a session that ended manually, rather than automatically.", - "isCommentAutoGenerated" : true - }, - "Ending balance" : { - }, "Example: 5♥ + 5♣ = Pair (wins!)" : { "comment" : "Example of a pair bet winning.", @@ -1819,10 +1783,6 @@ } } }, - "Global" : { - "comment" : "Title of the statistics tab that shows global statistics.", - "isCommentAutoGenerated" : true - }, "Green Circle (T): Tie between Player and Banker" : { "comment" : "Explains the green circle icon in the history.", "localizations" : { @@ -1919,10 +1879,6 @@ } } }, - "History" : { - "comment" : "Title of the statistics tab that shows the user's session history.", - "isCommentAutoGenerated" : true - }, "HISTORY" : { "comment" : "A label displayed above the road map view, indicating that it shows a history of past game results.", "localizations" : { @@ -2332,10 +2288,6 @@ } } }, - "Losing sessions" : { - "comment" : "A label describing the number of sessions the user has lost.", - "isCommentAutoGenerated" : true - }, "Lost" : { "comment" : "Labels for the outcome circles in the \"Win/Loss/Push\" section.", "isCommentAutoGenerated" : true @@ -2480,9 +2432,6 @@ "Net" : { "comment" : "Label for the net winnings in the statistics sheet.", "isCommentAutoGenerated" : true - }, - "Net result" : { - }, "Never" : { "localizations" : { @@ -2529,10 +2478,6 @@ } } }, - "No Active Session" : { - "comment" : "A message displayed when there is no active session to display in the statistics sheet.", - "isCommentAutoGenerated" : true - }, "No cards" : { "comment" : "A description of the player's hand when they have no cards.", "localizations" : { @@ -2601,10 +2546,6 @@ } } }, - "No Session History" : { - "comment" : "A description displayed when a user has no session history.", - "isCommentAutoGenerated" : true - }, "Objective" : { "comment" : "Title of a rule page in the \"Rules\" help view, describing the objective of the game.", "localizations" : { @@ -3126,10 +3067,6 @@ "comment" : "A label for the \"Push\" outcome in the game stats section.", "isCommentAutoGenerated" : true }, - "Ran out of chips" : { - "comment" : "A description of why a session might have ended with a \"Ran out of chips\" result.", - "isCommentAutoGenerated" : true - }, "Red Circle (B): Banker won the hand" : { "comment" : "Explains the red circle icon in the history.", "localizations" : { @@ -3529,15 +3466,8 @@ } } }, - "Start playing to begin tracking your session." : { - "comment" : "A description below the \"No Active Session\" label, instructing the user to start playing to view their session statistics.", - "isCommentAutoGenerated" : true - }, "Start with $1,000 and play risk-free" : { - }, - "Starting balance" : { - }, "STARTING BALANCE" : { "comment" : "Section header for starting balance settings.", @@ -3979,14 +3909,6 @@ } } }, - "Total bet" : { - "comment" : "The value string for the \"Total bet\" row in the statistics sheet.", - "isCommentAutoGenerated" : true - }, - "Total gain" : { - "comment" : "Label for the total gain in the statistics sheet.", - "isCommentAutoGenerated" : true - }, "Total game time" : { "comment" : "Rows in the \"Game stats\" section of the statistics sheet, showing various statistics about a Baccarat session.", "isCommentAutoGenerated" : true @@ -4269,10 +4191,6 @@ } } }, - "win rate" : { - "comment" : "A label describing the win rate of a session.", - "isCommentAutoGenerated" : true - }, "Win Rate" : { "comment" : "Label for the win rate in the statistics sheet.", "isCommentAutoGenerated" : true @@ -4300,22 +4218,10 @@ } } }, - "Winning sessions" : { - "comment" : "A title describing the number of sessions the user has won.", - "isCommentAutoGenerated" : true - }, "Won" : { "comment" : "Labels for the outcome circles in the \"Win/Loss/Push\" section.", "isCommentAutoGenerated" : true }, - "Worst loss" : { - "comment" : "The label and value for the \"Worst loss\" row are identical to those for the \"Best gain\" row. This is intentional, as it highlights the symmetry in the data.", - "isCommentAutoGenerated" : true - }, - "Worst session" : { - "comment" : "A label for the worst session amount in the statistics sheet.", - "isCommentAutoGenerated" : true - }, "Yellow Dot (bottom-left): A pair occurred in that hand" : { "comment" : "Explains the yellow dot marker in the history.", "localizations" : { diff --git a/Baccarat/Baccarat/Theme/BrandingConfig+Baccarat.swift b/Baccarat/Baccarat/Theme/BrandingConfig+Baccarat.swift new file mode 100644 index 0000000..240138d --- /dev/null +++ b/Baccarat/Baccarat/Theme/BrandingConfig+Baccarat.swift @@ -0,0 +1,40 @@ +// +// BrandingConfig+Baccarat.swift +// Baccarat +// +// Baccarat-specific branding configurations for AppIconView and LaunchScreenView. +// + +import SwiftUI +import CasinoKit + +extension AppIconConfig { + /// Baccarat game icon configuration. + static let baccarat = AppIconConfig( + title: "BACCARAT", + iconSymbol: "suit.spade.fill" + ) +} + +extension LaunchScreenConfig { + /// Baccarat game launch screen configuration. + static let baccarat = LaunchScreenConfig( + title: "BACCARAT", + tagline: "The Classic Casino Card Game", + iconSymbols: ["suit.spade.fill", "suit.heart.fill"] + ) +} + +// MARK: - For Development Preview Comparison + +extension AppIconConfig { + /// Blackjack config for side-by-side comparison in dev previews. + static let blackjack = AppIconConfig( + title: "BLACKJACK", + subtitle: "21", + iconSymbol: "suit.club.fill", + primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15), + secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1) + ) +} + diff --git a/Baccarat/Baccarat/Views/Game/GameTableView.swift b/Baccarat/Baccarat/Views/Game/GameTableView.swift index bc4c5ba..5eae418 100644 --- a/Baccarat/Baccarat/Views/Game/GameTableView.swift +++ b/Baccarat/Baccarat/Views/Game/GameTableView.swift @@ -136,6 +136,7 @@ struct GameTableView: View { .sheet(isPresented: $showWelcome) { WelcomeSheet( gameName: "Baccarat", + gameEmoji: "🎴", features: [ WelcomeFeature( icon: "hand.raised.fill", diff --git a/Baccarat/Baccarat/Views/Sheets/StatisticsSheetView.swift b/Baccarat/Baccarat/Views/Sheets/StatisticsSheetView.swift index 075b9dc..7a5b41e 100644 --- a/Baccarat/Baccarat/Views/Sheets/StatisticsSheetView.swift +++ b/Baccarat/Baccarat/Views/Sheets/StatisticsSheetView.swift @@ -19,8 +19,8 @@ struct StatisticsSheetView: View { SheetContainerView( title: String(localized: "Statistics"), content: { - // Tab selector - tabSelector + // Tab selector (from CasinoKit) + StatisticsTabSelector(selectedTab: $selectedTab) // Content based on selected tab switch selectedTab { @@ -67,34 +67,7 @@ struct StatisticsSheetView: View { } } - // MARK: - Tab Selector - - private var tabSelector: some View { - HStack(spacing: 0) { - ForEach(StatisticsTab.allCases, id: \.self) { tab in - Button { - withAnimation(.spring(duration: Design.Animation.springDuration)) { - selectedTab = tab - } - } label: { - VStack(spacing: Design.Spacing.xSmall) { - Image(systemName: tab.icon) - .font(.system(size: Design.BaseFontSize.large)) - Text(tab.title) - .font(.system(size: Design.BaseFontSize.small, weight: .medium)) - } - .foregroundStyle(selectedTab == tab ? Color.Sheet.accent : .white.opacity(Design.Opacity.medium)) - .frame(maxWidth: .infinity) - .padding(.vertical, Design.Spacing.medium) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(selectedTab == tab ? Color.Sheet.accent.opacity(Design.Opacity.hint) : .clear) - ) - } - } - } - .padding(.horizontal) - } + // MARK: - Tab Selector (uses CasinoKit.StatisticsTabSelector) // MARK: - Current Session Content @@ -119,28 +92,10 @@ struct StatisticsSheetView: View { bigRoadSection roadMapSection } else { - noActiveSessionView + NoActiveSessionView() } } - private var noActiveSessionView: some View { - VStack(spacing: Design.Spacing.large) { - Image(systemName: "play.slash") - .font(.system(size: Design.BaseFontSize.xxLarge * 2)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - - Text(String(localized: "No Active Session")) - .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Text(String(localized: "Start playing to begin tracking your session.")) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - .multilineTextAlignment(.center) - } - .padding(Design.Spacing.xxLarge) - } - // MARK: - Global Stats Content private var globalStatsContent: some View { @@ -215,43 +170,14 @@ struct StatisticsSheetView: View { } } - // Session performance + // Session performance (from CasinoKit) SheetSection(title: String(localized: "SESSION PERFORMANCE"), icon: "chart.bar.fill") { - VStack(spacing: Design.Spacing.medium) { - HStack { - Text(String(localized: "Winning sessions")) - Spacer() - Text("\(stats.winningSessions)") - .foregroundStyle(.green) - .bold() - } - HStack { - Text(String(localized: "Losing sessions")) - Spacer() - Text("\(stats.losingSessions)") - .foregroundStyle(.red) - .bold() - } - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - HStack { - Text(String(localized: "Best session")) - Spacer() - Text(SessionFormatter.formatMoney(stats.bestSession)) - .foregroundStyle(.green) - .bold() - } - HStack { - Text(String(localized: "Worst session")) - Spacer() - Text(SessionFormatter.formatMoney(stats.worstSession)) - .foregroundStyle(.red) - .bold() - } - } - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) + SessionPerformanceSection( + winningSessions: stats.winningSessions, + losingSessions: stats.losingSessions, + bestSession: stats.bestSession, + worstSession: stats.worstSession + ) } } } @@ -261,7 +187,7 @@ struct StatisticsSheetView: View { private var sessionHistoryContent: some View { Group { if state.sessionHistory.isEmpty && state.currentSession == nil { - emptyHistoryView + EmptyHistoryView() } else { LazyVStack(spacing: Design.Spacing.medium) { // Current session at top if exists (taps go to Current tab) @@ -322,23 +248,6 @@ struct StatisticsSheetView: View { } } - private var emptyHistoryView: some View { - VStack(spacing: Design.Spacing.large) { - Image(systemName: "clock.arrow.circlepath") - .font(.system(size: Design.BaseFontSize.xxLarge * 2)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - - Text(String(localized: "No Session History")) - .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Text(String(localized: "Completed sessions will appear here.")) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - .multilineTextAlignment(.center) - } - .padding(Design.Spacing.xxLarge) - } // MARK: - Session Stats Section @@ -396,55 +305,16 @@ struct StatisticsSheetView: View { } } - // Chips stats section + // Chips stats section (from CasinoKit) SheetSection(title: String(localized: "CHIPS STATS"), icon: "dollarsign.circle.fill") { - VStack(spacing: Design.Spacing.medium) { - ChipStatRow( - icon: "chart.line.uptrend.xyaxis", - iconColor: session.totalWinnings >= 0 ? .green : .red, - label: String(localized: "Total gain"), - value: SessionFormatter.formatMoney(session.totalWinnings) - ) - - ChipStatRow( - icon: "arrow.up.circle.fill", - iconColor: .green, - label: String(localized: "Best gain"), - value: SessionFormatter.formatMoney(session.biggestWin) - ) - - ChipStatRow( - icon: "arrow.down.circle.fill", - iconColor: .red, - label: String(localized: "Worst loss"), - value: SessionFormatter.formatMoney(session.biggestLoss) - ) - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - ChipStatRow( - icon: "plusminus.circle.fill", - iconColor: .blue, - label: String(localized: "Total bet"), - value: "$\(session.totalBetAmount)" - ) - - if session.roundsPlayed > 0 { - ChipStatRow( - icon: "equal.circle.fill", - iconColor: .purple, - label: String(localized: "Average bet"), - value: "$\(session.averageBet)" - ) - } - - ChipStatRow( - icon: "star.circle.fill", - iconColor: .orange, - label: String(localized: "Biggest bet"), - value: "$\(session.biggestBet)" - ) - } + ChipsStatsSection( + totalWinnings: session.totalWinnings, + biggestWin: session.biggestWin, + biggestLoss: session.biggestLoss, + totalBetAmount: session.totalBetAmount, + averageBet: session.roundsPlayed > 0 ? session.averageBet : nil, + biggestBet: session.biggestBet + ) } } @@ -501,163 +371,37 @@ struct StatisticsSheetView: View { } } -// MARK: - Statistics Tab +// MARK: - Statistics Tab & Supporting Views +// StatisticsTab, StatColumn, OutcomeCircle, StatRow, ChipStatRow are now provided by CasinoKit -private enum StatisticsTab: CaseIterable { - case current - case global - case history - - var title: String { - switch self { - case .current: return String(localized: "Current") - case .global: return String(localized: "Global") - case .history: return String(localized: "History") - } - } - - var icon: String { - switch self { - case .current: return "play.circle.fill" - case .global: return "globe" - case .history: return "clock.arrow.circlepath" - } - } -} - -// MARK: - Supporting Views - -private struct StatColumn: View { - let value: String - let label: String - var valueColor: Color = .white - - var body: some View { - VStack(spacing: Design.Spacing.xSmall) { - Text(value) - .font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .rounded)) - .foregroundStyle(valueColor) - Text(label) - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .frame(maxWidth: .infinity) - } -} - -private struct OutcomeCircle: View { - let label: String - let count: Int - let color: Color - - var body: some View { - VStack(spacing: Design.Spacing.small) { - Text(label) - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Text("\(count)") - .font(.system(size: Design.BaseFontSize.large, weight: .bold)) - .foregroundStyle(.white) - - ZStack { - Circle() - .fill(Color.black.opacity(Design.Opacity.light)) - .frame(width: Size.outcomeCircleSize, height: Size.outcomeCircleSize) - - Circle() - .stroke(color, lineWidth: Design.LineWidth.thick) - .frame(width: Size.outcomeCircleSize, height: Size.outcomeCircleSize) - - Circle() - .fill(color.opacity(Design.Opacity.medium)) - .frame(width: Size.outcomeCircleInner, height: Size.outcomeCircleInner) - } - } - .frame(maxWidth: .infinity) - } -} +// MARK: - Baccarat-Specific Views +/// Compact win distribution indicator for Player/Banker/Tie. private struct WinStatCompact: View { let label: String let count: Int let color: Color + private let indicatorSize: CGFloat = 24 + var body: some View { - VStack(spacing: Design.Spacing.xSmall) { + VStack(spacing: CasinoDesign.Spacing.xSmall) { Circle() .fill(color) - .frame(width: Size.winIndicatorSize, height: Size.winIndicatorSize) + .frame(width: indicatorSize, height: indicatorSize) Text("\(count)") - .font(.system(size: Design.BaseFontSize.large, weight: .bold)) + .font(.system(size: CasinoDesign.BaseFontSize.large, weight: .bold)) .foregroundStyle(.white) Text(label) - .font(.system(size: Design.BaseFontSize.small)) + .font(.system(size: CasinoDesign.BaseFontSize.small)) .foregroundStyle(color) } .frame(maxWidth: .infinity) } } -private struct StatRow: View { - let icon: String - let label: String - let value: String - var valueColor: Color = .white - - var body: some View { - HStack { - Image(systemName: icon) - .font(.system(size: Design.BaseFontSize.large)) - .foregroundStyle(Color.Sheet.accent) - .frame(width: Size.statIconWidth) - - Text(label) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - - Spacer() - - Text(value) - .font(.system(size: Design.BaseFontSize.body, weight: .semibold, design: .rounded)) - .foregroundStyle(valueColor) - } - } -} - -private struct ChipStatRow: View { - let icon: String - let iconColor: Color - let label: String - let value: String - - var body: some View { - HStack { - ZStack { - Circle() - .fill(iconColor) - .frame(width: Size.chipIconSize, height: Size.chipIconSize) - - Image(systemName: icon) - .font(.system(size: Design.BaseFontSize.small, weight: .bold)) - .foregroundStyle(.white) - } - - Text(label) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - - Spacer() - - Text(value) - .font(.system(size: Design.BaseFontSize.body, weight: .semibold, design: .rounded)) - .foregroundStyle(Color.Sheet.accent) - } - } -} - // MARK: - Session Detail View private struct SessionDetailView: View { @@ -673,8 +417,13 @@ private struct SessionDetailView: View { SheetContainerView( title: styleDisplayName, content: { - // Session header info - sessionHeader + // Session header info (from CasinoKit) + SessionDetailHeader( + startTime: session.startTime, + endReason: session.endReason, + netResult: session.netResult, + winRate: session.winRate + ) // Game stats section SheetSection(title: String(localized: "GAME STATS"), icon: "gamecontroller.fill") { @@ -689,7 +438,7 @@ private struct SessionDetailView: View { .foregroundStyle(.white) } - // Win/Loss/Push + // Win/Loss/Push (from CasinoKit) HStack(spacing: Design.Spacing.medium) { OutcomeCircle(label: String(localized: "Won"), count: session.wins, color: .white) OutcomeCircle(label: String(localized: "Lost"), count: session.losses, color: .red) @@ -698,7 +447,7 @@ private struct SessionDetailView: View { Divider().background(Color.white.opacity(Design.Opacity.hint)) - // Game time + // Game time (from CasinoKit) StatRow(icon: "clock", label: String(localized: "Total game time"), value: SessionFormatter.formatDuration(session.duration)) // Baccarat-specific stats @@ -708,7 +457,7 @@ private struct SessionDetailView: View { Divider().background(Color.white.opacity(Design.Opacity.hint)) - // Win distribution + // Win distribution (Baccarat-specific) HStack(spacing: Design.Spacing.large) { WinStatCompact( label: String(localized: "Player"), @@ -729,79 +478,28 @@ private struct SessionDetailView: View { } } - // Chips stats section + // Chips stats section (from CasinoKit) SheetSection(title: String(localized: "CHIPS STATS"), icon: "dollarsign.circle.fill") { - VStack(spacing: Design.Spacing.medium) { - ChipStatRow( - icon: "chart.line.uptrend.xyaxis", - iconColor: session.totalWinnings >= 0 ? .green : .red, - label: String(localized: "Net result"), - value: SessionFormatter.formatMoney(session.totalWinnings) - ) - - ChipStatRow( - icon: "arrow.up.circle.fill", - iconColor: .green, - label: String(localized: "Best gain"), - value: SessionFormatter.formatMoney(session.biggestWin) - ) - - ChipStatRow( - icon: "arrow.down.circle.fill", - iconColor: .red, - label: String(localized: "Worst loss"), - value: SessionFormatter.formatMoney(session.biggestLoss) - ) - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - ChipStatRow( - icon: "plusminus.circle.fill", - iconColor: .blue, - label: String(localized: "Total bet"), - value: "$\(session.totalBetAmount)" - ) - - if session.roundsPlayed > 0 { - ChipStatRow( - icon: "equal.circle.fill", - iconColor: .purple, - label: String(localized: "Average bet"), - value: "$\(session.averageBet)" - ) - } - - ChipStatRow( - icon: "star.circle.fill", - iconColor: .orange, - label: String(localized: "Biggest bet"), - value: "$\(session.biggestBet)" - ) - } + ChipsStatsSection( + totalWinnings: session.totalWinnings, + biggestWin: session.biggestWin, + biggestLoss: session.biggestLoss, + totalBetAmount: session.totalBetAmount, + averageBet: session.roundsPlayed > 0 ? session.averageBet : nil, + biggestBet: session.biggestBet + ) } - // Balance section + // Balance section (from CasinoKit) SheetSection(title: String(localized: "BALANCE"), icon: "banknote.fill") { - VStack(spacing: Design.Spacing.medium) { - HStack { - Text(String(localized: "Starting balance")) - Spacer() - Text("$\(session.startingBalance)") - .bold() - } - HStack { - Text(String(localized: "Ending balance")) - Spacer() - Text("$\(session.endingBalance)") - .foregroundStyle(session.netResult >= 0 ? .green : .red) - .bold() - } - } - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) + BalanceSection( + startingBalance: session.startingBalance, + endingBalance: session.endingBalance, + netResult: session.netResult + ) } - // Big Road section + // Big Road section (Baccarat-specific) if !roundHistory.isEmpty { SheetSection(title: String(localized: "BIG ROAD"), icon: "chart.bar.xaxis") { BigRoadView(results: roundHistory) @@ -827,25 +525,10 @@ private struct SessionDetailView: View { } } - // Delete button - Button(role: .destructive) { + // Delete button (from CasinoKit) + DeleteSessionButton { showDeleteConfirmation = true - } label: { - HStack { - Image(systemName: "trash") - Text(String(localized: "Delete Session")) - } - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .frame(maxWidth: .infinity) - .padding(Design.Spacing.medium) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(Color.red.opacity(Design.Opacity.hint)) - ) - .foregroundStyle(.red) } - .padding(.horizontal) - .padding(.top, Design.Spacing.large) }, onCancel: nil, onDone: { dismiss() }, @@ -865,48 +548,6 @@ private struct SessionDetailView: View { Text(String(localized: "This will permanently remove this session from your history.")) } } - - private var sessionHeader: some View { - VStack(spacing: Design.Spacing.small) { - // Date and duration - HStack { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text(SessionFormatter.formatSessionDate(session.startTime)) - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.white) - - if let endReason = session.endReason { - HStack(spacing: Design.Spacing.xSmall) { - Image(systemName: endReason == .brokeOut ? "xmark.circle.fill" : "checkmark.circle.fill") - .foregroundStyle(endReason == .brokeOut ? .red : .green) - Text(endReason == .brokeOut ? String(localized: "Ran out of chips") : String(localized: "Ended manually")) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .font(.system(size: Design.BaseFontSize.small)) - } - } - - Spacer() - - // Net result badge - VStack(alignment: .trailing, spacing: Design.Spacing.xxSmall) { - Text(SessionFormatter.formatMoney(session.netResult)) - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded)) - .foregroundStyle(session.netResult >= 0 ? .green : .red) - - Text(SessionFormatter.formatPercent(session.winRate) + " " + String(localized: "win rate")) - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - } - } - .padding() - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(Color.Sheet.sectionFill) - ) - .padding(.horizontal) - } } // MARK: - Big Road View diff --git a/Blackjack/Blackjack/Engine/GameState.swift b/Blackjack/Blackjack/Engine/GameState.swift index 42bece2..6dea790 100644 --- a/Blackjack/Blackjack/Engine/GameState.swift +++ b/Blackjack/Blackjack/Engine/GameState.swift @@ -19,12 +19,14 @@ enum GamePhase: Equatable { } /// Main game state manager. +/// Conforms to CasinoGameState for shared game behaviors. @Observable @MainActor -final class GameState: SessionManagedGame { - // MARK: - SessionManagedGame +final class GameState: CasinoGameState { + // MARK: - CasinoGameState Conformance typealias Stats = BlackjackStats + typealias GameSettingsType = GameSettings /// Current player balance. var balance: Int diff --git a/Blackjack/Blackjack/Models/GameSettings.swift b/Blackjack/Blackjack/Models/GameSettings.swift index d68e81f..54f3bd1 100644 --- a/Blackjack/Blackjack/Models/GameSettings.swift +++ b/Blackjack/Blackjack/Models/GameSettings.swift @@ -69,9 +69,10 @@ enum DeckCount: Int, CaseIterable, Identifiable { } /// Observable settings class for Blackjack configuration. +/// Conforms to GameSettingsProtocol for shared settings behavior. @Observable @MainActor -final class GameSettings { +final class GameSettings: GameSettingsProtocol { // MARK: - Game Style /// The preset rule variation. diff --git a/Blackjack/Blackjack/Resources/Localizable.xcstrings b/Blackjack/Blackjack/Resources/Localizable.xcstrings index 347176d..c324ffd 100644 --- a/Blackjack/Blackjack/Resources/Localizable.xcstrings +++ b/Blackjack/Blackjack/Resources/Localizable.xcstrings @@ -399,10 +399,6 @@ } } }, - "$%lld" : { - "comment" : "The starting balance of a session, displayed in bold text.", - "isCommentAutoGenerated" : true - }, "1 Deck: Lowest house edge (~0.17%), rare to find." : { "localizations" : { "en" : { @@ -1088,10 +1084,6 @@ } } }, - "Average bet" : { - "comment" : "Label for the average bet value in the Statistics Sheet.", - "isCommentAutoGenerated" : true - }, "Baccarat" : { "comment" : "The name of a casino game.", "isCommentAutoGenerated" : true, @@ -1237,14 +1229,6 @@ } } }, - "Best gain" : { - "comment" : "Label in the statistics sheet for the player's best single win.", - "isCommentAutoGenerated" : true - }, - "Best session" : { - "comment" : "A label describing the best session a user has played.", - "isCommentAutoGenerated" : true - }, "Bet 2x minimum" : { "comment" : "Betting recommendation based on a true count of 1.", "isCommentAutoGenerated" : true, @@ -1508,10 +1492,6 @@ } } }, - "Biggest bet" : { - "comment" : "Label for the \"Biggest bet\" row in the Statistics Sheet.", - "isCommentAutoGenerated" : true - }, "BIGGEST SWINGS" : { "extractionState" : "stale", "localizations" : { @@ -2081,10 +2061,6 @@ } } }, - "Completed sessions will appear here." : { - "comment" : "A description below the label \"Your Session History\" in the StatisticsSheetView, explaining that completed sessions will be listed there.", - "isCommentAutoGenerated" : true - }, "Cost: $%lld (half your bet)" : { "localizations" : { "en" : { @@ -2176,9 +2152,6 @@ } } } - }, - "Current" : { - }, "Current bet $%lld" : { "comment" : "A hint that appears when a user taps on a side bet zone. The text varies depending on whether a bet is currently placed or not.", @@ -2663,9 +2636,6 @@ "Delete" : { "comment" : "A button label that deletes a session.", "isCommentAutoGenerated" : true - }, - "Delete Session" : { - }, "Delete Session?" : { @@ -3151,14 +3121,6 @@ "comment" : "A confirmation dialog title that asks if the user wants to end their current session.", "isCommentAutoGenerated" : true }, - "Ended manually" : { - "comment" : "A description of a session that ended manually (e.g. by the user closing the game).", - "isCommentAutoGenerated" : true - }, - "Ending balance" : { - "comment" : "A label displayed below the user's ending balance in the session detail view.", - "isCommentAutoGenerated" : true - }, "European" : { "localizations" : { "en" : { @@ -3483,10 +3445,6 @@ }, "Get closer to 21 than the dealer without going over" : { - }, - "Global" : { - "comment" : "Title for the \"Global\" tab in the statistics sheet.", - "isCommentAutoGenerated" : true }, "H17 rule, increases house edge" : { "localizations" : { @@ -3630,10 +3588,6 @@ } } }, - "History" : { - "comment" : "Title of the statistics tab that shows the user's session history.", - "isCommentAutoGenerated" : true - }, "Hit" : { "localizations" : { "en" : { @@ -4256,10 +4210,6 @@ } } }, - "Losing sessions" : { - "comment" : "A label describing the number of sessions that the user lost.", - "isCommentAutoGenerated" : true - }, "Losses" : { "extractionState" : "stale", "localizations" : { @@ -4561,10 +4511,6 @@ } } }, - "Net result" : { - "comment" : "Label for a row in the \"Chips stats\" section of the session detail view, showing the net result of the session (i.e. the difference between the starting and ending balance).", - "isCommentAutoGenerated" : true - }, "Never" : { "localizations" : { "en" : { @@ -4632,10 +4578,6 @@ } } }, - "No Active Session" : { - "comment" : "A message displayed when there is no active blackjack session to display statistics for.", - "isCommentAutoGenerated" : true - }, "No Hand" : { "comment" : "Description of a 21+3 side bet outcome when there is no qualifying hand.", "isCommentAutoGenerated" : true, @@ -4727,10 +4669,6 @@ } } }, - "No Session History" : { - "comment" : "A message displayed when a user has no session history.", - "isCommentAutoGenerated" : true - }, "No surrender option." : { "localizations" : { "en" : { @@ -5336,10 +5274,6 @@ } } }, - "Ran out of chips" : { - "comment" : "A description of why a blackjack session ended when the player ran out of chips.", - "isCommentAutoGenerated" : true - }, "Re-split Aces" : { "localizations" : { "en" : { @@ -6341,14 +6275,6 @@ } } }, - "Start playing to begin tracking your session." : { - "comment" : "A description text displayed in the \"No Active Session\" view, explaining that the user needs to start playing to see their session statistics.", - "isCommentAutoGenerated" : true - }, - "Starting balance" : { - "comment" : "A label for the starting balance in the Balance section of a session detail view.", - "isCommentAutoGenerated" : true - }, "STARTING BALANCE" : { "localizations" : { "en" : { @@ -6879,14 +6805,6 @@ "comment" : "Label for the duration of a blackjack game.", "isCommentAutoGenerated" : true }, - "Total bet" : { - "comment" : "Label for the total bet value in the Statistics Sheet.", - "isCommentAutoGenerated" : true - }, - "Total gain" : { - "comment" : "Label in the Statistics sheet for the total gain (profit or loss) from playing blackjack.", - "isCommentAutoGenerated" : true - }, "Total game time" : { "comment" : "Label for a stat row displaying the total game time.", "isCommentAutoGenerated" : true @@ -7101,10 +7019,6 @@ } } }, - "win rate" : { - "comment" : "A description of what \"win rate\" means in the context of a casino game.", - "isCommentAutoGenerated" : true - }, "Win Rate" : { "localizations" : { "en" : { @@ -7173,10 +7087,6 @@ } } }, - "Winning sessions" : { - "comment" : "A label describing the number of sessions the user has won.", - "isCommentAutoGenerated" : true - }, "Wins" : { "extractionState" : "stale", "localizations" : { @@ -7227,14 +7137,6 @@ } } }, - "Worst loss" : { - "comment" : "Description of a chip stat row when displaying the worst loss.", - "isCommentAutoGenerated" : true - }, - "Worst session" : { - "comment" : "A label for the worst session's winnings in the statistics sheet.", - "isCommentAutoGenerated" : true - }, "Yes ($%lld)" : { "localizations" : { "en" : { diff --git a/Blackjack/Blackjack/Theme/BrandingConfig+Blackjack.swift b/Blackjack/Blackjack/Theme/BrandingConfig+Blackjack.swift new file mode 100644 index 0000000..d56716e --- /dev/null +++ b/Blackjack/Blackjack/Theme/BrandingConfig+Blackjack.swift @@ -0,0 +1,43 @@ +// +// BrandingConfig+Blackjack.swift +// Blackjack +// +// Blackjack-specific branding configurations for AppIconView and LaunchScreenView. +// + +import SwiftUI +import CasinoKit + +extension AppIconConfig { + /// Blackjack game icon configuration. + static let blackjack = AppIconConfig( + title: "BLACKJACK", + subtitle: "21", + iconSymbol: "suit.club.fill", + primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15), + secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1) + ) +} + +extension LaunchScreenConfig { + /// Blackjack game launch screen configuration. + static let blackjack = LaunchScreenConfig( + title: "BLACKJACK", + subtitle: "21", + tagline: "Beat the Dealer", + iconSymbols: ["suit.club.fill", "suit.diamond.fill"], + primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15), + secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1) + ) +} + +// MARK: - For Development Preview Comparison + +extension AppIconConfig { + /// Baccarat config for side-by-side comparison in dev previews. + static let baccarat = AppIconConfig( + title: "BACCARAT", + iconSymbol: "suit.spade.fill" + ) +} + diff --git a/Blackjack/Blackjack/Views/Game/GameTableView.swift b/Blackjack/Blackjack/Views/Game/GameTableView.swift index 58468f3..25b6e14 100644 --- a/Blackjack/Blackjack/Views/Game/GameTableView.swift +++ b/Blackjack/Blackjack/Views/Game/GameTableView.swift @@ -86,6 +86,7 @@ struct GameTableView: View { .sheet(isPresented: $showWelcome) { WelcomeSheet( gameName: "Blackjack", + gameEmoji: "🃏", features: [ WelcomeFeature( icon: "target", diff --git a/Blackjack/Blackjack/Views/Sheets/StatisticsSheetView.swift b/Blackjack/Blackjack/Views/Sheets/StatisticsSheetView.swift index edec5f4..95fe90c 100644 --- a/Blackjack/Blackjack/Views/Sheets/StatisticsSheetView.swift +++ b/Blackjack/Blackjack/Views/Sheets/StatisticsSheetView.swift @@ -19,8 +19,8 @@ struct StatisticsSheetView: View { SheetContainerView( title: String(localized: "Statistics"), content: { - // Tab selector - tabSelector + // Tab selector (from CasinoKit) + StatisticsTabSelector(selectedTab: $selectedTab) // Content based on selected tab switch selectedTab { @@ -63,34 +63,7 @@ struct StatisticsSheetView: View { } } - // MARK: - Tab Selector - - private var tabSelector: some View { - HStack(spacing: 0) { - ForEach(StatisticsTab.allCases, id: \.self) { tab in - Button { - withAnimation(.spring(duration: Design.Animation.springDuration)) { - selectedTab = tab - } - } label: { - VStack(spacing: Design.Spacing.xSmall) { - Image(systemName: tab.icon) - .font(.system(size: Design.BaseFontSize.large)) - Text(tab.title) - .font(.system(size: Design.BaseFontSize.small, weight: .medium)) - } - .foregroundStyle(selectedTab == tab ? Color.Sheet.accent : .white.opacity(Design.Opacity.medium)) - .frame(maxWidth: .infinity) - .padding(.vertical, Design.Spacing.medium) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(selectedTab == tab ? Color.Sheet.accent.opacity(Design.Opacity.hint) : .clear) - ) - } - } - } - .padding(.horizontal) - } + // MARK: - Tab Selector (uses CasinoKit.StatisticsTabSelector) // MARK: - Current Session Content @@ -111,28 +84,10 @@ struct StatisticsSheetView: View { // Session stats sessionStatsSection(session: session) } else { - noActiveSessionView + NoActiveSessionView() } } - private var noActiveSessionView: some View { - VStack(spacing: Design.Spacing.large) { - Image(systemName: "play.slash") - .font(.system(size: Design.BaseFontSize.xxLarge * 2)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - - Text(String(localized: "No Active Session")) - .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Text(String(localized: "Start playing to begin tracking your session.")) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - .multilineTextAlignment(.center) - } - .padding(Design.Spacing.xxLarge) - } - // MARK: - Global Stats Content private var globalStatsContent: some View { @@ -186,43 +141,14 @@ struct StatisticsSheetView: View { } } - // Session performance + // Session performance (from CasinoKit) SheetSection(title: String(localized: "SESSION PERFORMANCE"), icon: "chart.bar.fill") { - VStack(spacing: Design.Spacing.medium) { - HStack { - Text(String(localized: "Winning sessions")) - Spacer() - Text("\(stats.winningSessions)") - .foregroundStyle(.green) - .bold() - } - HStack { - Text(String(localized: "Losing sessions")) - Spacer() - Text("\(stats.losingSessions)") - .foregroundStyle(.red) - .bold() - } - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - HStack { - Text(String(localized: "Best session")) - Spacer() - Text(SessionFormatter.formatMoney(stats.bestSession)) - .foregroundStyle(.green) - .bold() - } - HStack { - Text(String(localized: "Worst session")) - Spacer() - Text(SessionFormatter.formatMoney(stats.worstSession)) - .foregroundStyle(.red) - .bold() - } - } - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) + SessionPerformanceSection( + winningSessions: stats.winningSessions, + losingSessions: stats.losingSessions, + bestSession: stats.bestSession, + worstSession: stats.worstSession + ) } } } @@ -232,7 +158,7 @@ struct StatisticsSheetView: View { private var sessionHistoryContent: some View { Group { if state.sessionHistory.isEmpty && state.currentSession == nil { - emptyHistoryView + EmptyHistoryView() } else { LazyVStack(spacing: Design.Spacing.medium) { // Current session at top if exists (taps go to Current tab) @@ -293,23 +219,6 @@ struct StatisticsSheetView: View { } } - private var emptyHistoryView: some View { - VStack(spacing: Design.Spacing.large) { - Image(systemName: "clock.arrow.circlepath") - .font(.system(size: Design.BaseFontSize.xxLarge * 2)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - - Text(String(localized: "No Session History")) - .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Text(String(localized: "Completed sessions will appear here.")) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - .multilineTextAlignment(.center) - } - .padding(Design.Spacing.xxLarge) - } // MARK: - Session Stats Section @@ -346,55 +255,16 @@ struct StatisticsSheetView: View { } } - // Chips stats section + // Chips stats section (from CasinoKit) SheetSection(title: String(localized: "CHIPS STATS"), icon: "dollarsign.circle.fill") { - VStack(spacing: Design.Spacing.medium) { - ChipStatRow( - icon: "chart.line.uptrend.xyaxis", - iconColor: session.totalWinnings >= 0 ? .green : .red, - label: String(localized: "Total gain"), - value: SessionFormatter.formatMoney(session.totalWinnings) - ) - - ChipStatRow( - icon: "arrow.up.circle.fill", - iconColor: .green, - label: String(localized: "Best gain"), - value: SessionFormatter.formatMoney(session.biggestWin) - ) - - ChipStatRow( - icon: "arrow.down.circle.fill", - iconColor: .red, - label: String(localized: "Worst loss"), - value: SessionFormatter.formatMoney(session.biggestLoss) - ) - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - ChipStatRow( - icon: "plusminus.circle.fill", - iconColor: .blue, - label: String(localized: "Total bet"), - value: "$\(session.totalBetAmount)" - ) - - if session.roundsPlayed > 0 { - ChipStatRow( - icon: "equal.circle.fill", - iconColor: .purple, - label: String(localized: "Average bet"), - value: "$\(session.averageBet)" - ) - } - - ChipStatRow( - icon: "star.circle.fill", - iconColor: .orange, - label: String(localized: "Biggest bet"), - value: "$\(session.biggestBet)" - ) - } + ChipsStatsSection( + totalWinnings: session.totalWinnings, + biggestWin: session.biggestWin, + biggestLoss: session.biggestLoss, + totalBetAmount: session.totalBetAmount, + averageBet: session.roundsPlayed > 0 ? session.averageBet : nil, + biggestBet: session.biggestBet + ) } } @@ -405,139 +275,8 @@ struct StatisticsSheetView: View { } } -// MARK: - Statistics Tab - -private enum StatisticsTab: CaseIterable { - case current - case global - case history - - var title: String { - switch self { - case .current: return String(localized: "Current") - case .global: return String(localized: "Global") - case .history: return String(localized: "History") - } - } - - var icon: String { - switch self { - case .current: return "play.circle.fill" - case .global: return "globe" - case .history: return "clock.arrow.circlepath" - } - } -} - -// MARK: - Supporting Views - -private struct StatColumn: View { - let value: String - let label: String - var valueColor: Color = .white - - var body: some View { - VStack(spacing: Design.Spacing.xSmall) { - Text(value) - .font(.system(size: Design.BaseFontSize.large, weight: .bold, design: .rounded)) - .foregroundStyle(valueColor) - Text(label) - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .frame(maxWidth: .infinity) - } -} - -private struct OutcomeCircle: View { - let label: String - let count: Int - let color: Color - - var body: some View { - VStack(spacing: Design.Spacing.small) { - Text(label) - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Text("\(count)") - .font(.system(size: Design.BaseFontSize.large, weight: .bold)) - .foregroundStyle(.white) - - ZStack { - Circle() - .fill(Color.black.opacity(Design.Opacity.light)) - .frame(width: Size.outcomeCircleSize, height: Size.outcomeCircleSize) - - Circle() - .stroke(color, lineWidth: Design.LineWidth.thick) - .frame(width: Size.outcomeCircleSize, height: Size.outcomeCircleSize) - - Circle() - .fill(color.opacity(Design.Opacity.medium)) - .frame(width: Size.outcomeCircleInner, height: Size.outcomeCircleInner) - } - } - .frame(maxWidth: .infinity) - } -} - -private struct StatRow: View { - let icon: String - let label: String - let value: String - var valueColor: Color = .white - - var body: some View { - HStack { - Image(systemName: icon) - .font(.system(size: Design.BaseFontSize.large)) - .foregroundStyle(Color.Sheet.accent) - .frame(width: Size.statIconWidth) - - Text(label) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - - Spacer() - - Text(value) - .font(.system(size: Design.BaseFontSize.body, weight: .semibold, design: .rounded)) - .foregroundStyle(valueColor) - } - } -} - -private struct ChipStatRow: View { - let icon: String - let iconColor: Color - let label: String - let value: String - - var body: some View { - HStack { - ZStack { - Circle() - .fill(iconColor) - .frame(width: Size.chipIconSize, height: Size.chipIconSize) - - Image(systemName: icon) - .font(.system(size: Design.BaseFontSize.small, weight: .bold)) - .foregroundStyle(.white) - } - - Text(label) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - - Spacer() - - Text(value) - .font(.system(size: Design.BaseFontSize.body, weight: .semibold, design: .rounded)) - .foregroundStyle(Color.Sheet.accent) - } - } -} +// MARK: - Statistics Tab & Supporting Views +// StatisticsTab, StatColumn, OutcomeCircle, StatRow, ChipStatRow are now provided by CasinoKit // MARK: - Session Detail View @@ -553,8 +292,13 @@ private struct SessionDetailView: View { SheetContainerView( title: styleDisplayName, content: { - // Session header info - sessionHeader + // Session header info (from CasinoKit) + SessionDetailHeader( + startTime: session.startTime, + endReason: session.endReason, + netResult: session.netResult, + winRate: session.winRate + ) // Game stats section SheetSection(title: String(localized: "GAME STATS"), icon: "gamecontroller.fill") { @@ -569,7 +313,7 @@ private struct SessionDetailView: View { .foregroundStyle(.white) } - // Win/Loss/Push + // Win/Loss/Push (from CasinoKit) HStack(spacing: Design.Spacing.medium) { OutcomeCircle(label: String(localized: "Won"), count: session.wins, color: .white) OutcomeCircle(label: String(localized: "Lost"), count: session.losses, color: .red) @@ -578,7 +322,7 @@ private struct SessionDetailView: View { Divider().background(Color.white.opacity(Design.Opacity.hint)) - // Game time + // Game time (from CasinoKit) StatRow(icon: "clock", label: String(localized: "Total game time"), value: SessionFormatter.formatDuration(session.duration)) // Blackjack-specific stats @@ -588,97 +332,31 @@ private struct SessionDetailView: View { } } - // Chips stats section + // Chips stats section (from CasinoKit) SheetSection(title: String(localized: "CHIPS STATS"), icon: "dollarsign.circle.fill") { - VStack(spacing: Design.Spacing.medium) { - ChipStatRow( - icon: "chart.line.uptrend.xyaxis", - iconColor: session.totalWinnings >= 0 ? .green : .red, - label: String(localized: "Net result"), - value: SessionFormatter.formatMoney(session.totalWinnings) - ) - - ChipStatRow( - icon: "arrow.up.circle.fill", - iconColor: .green, - label: String(localized: "Best gain"), - value: SessionFormatter.formatMoney(session.biggestWin) - ) - - ChipStatRow( - icon: "arrow.down.circle.fill", - iconColor: .red, - label: String(localized: "Worst loss"), - value: SessionFormatter.formatMoney(session.biggestLoss) - ) - - Divider().background(Color.white.opacity(Design.Opacity.hint)) - - ChipStatRow( - icon: "plusminus.circle.fill", - iconColor: .blue, - label: String(localized: "Total bet"), - value: "$\(session.totalBetAmount)" - ) - - if session.roundsPlayed > 0 { - ChipStatRow( - icon: "equal.circle.fill", - iconColor: .purple, - label: String(localized: "Average bet"), - value: "$\(session.averageBet)" - ) - } - - ChipStatRow( - icon: "star.circle.fill", - iconColor: .orange, - label: String(localized: "Biggest bet"), - value: "$\(session.biggestBet)" - ) - } - } - - // Balance section - SheetSection(title: String(localized: "BALANCE"), icon: "banknote.fill") { - VStack(spacing: Design.Spacing.medium) { - HStack { - Text(String(localized: "Starting balance")) - Spacer() - Text("$\(session.startingBalance)") - .bold() - } - HStack { - Text(String(localized: "Ending balance")) - Spacer() - Text("$\(session.endingBalance)") - .foregroundStyle(session.netResult >= 0 ? .green : .red) - .bold() - } - } - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - } - - // Delete button - Button(role: .destructive) { - showDeleteConfirmation = true - } label: { - HStack { - Image(systemName: "trash") - Text(String(localized: "Delete Session")) - } - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .frame(maxWidth: .infinity) - .padding(Design.Spacing.medium) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(Color.red.opacity(Design.Opacity.hint)) + ChipsStatsSection( + totalWinnings: session.totalWinnings, + biggestWin: session.biggestWin, + biggestLoss: session.biggestLoss, + totalBetAmount: session.totalBetAmount, + averageBet: session.roundsPlayed > 0 ? session.averageBet : nil, + biggestBet: session.biggestBet ) - .foregroundStyle(.red) } - .padding(.horizontal) - .padding(.top, Design.Spacing.large) + + // Balance section (from CasinoKit) + SheetSection(title: String(localized: "BALANCE"), icon: "banknote.fill") { + BalanceSection( + startingBalance: session.startingBalance, + endingBalance: session.endingBalance, + netResult: session.netResult + ) + } + + // Delete button (from CasinoKit) + DeleteSessionButton { + showDeleteConfirmation = true + } }, onCancel: nil, onDone: { dismiss() }, @@ -698,57 +376,6 @@ private struct SessionDetailView: View { Text(String(localized: "This will permanently remove this session from your history.")) } } - - private var sessionHeader: some View { - VStack(spacing: Design.Spacing.small) { - // Date and duration - HStack { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text(SessionFormatter.formatSessionDate(session.startTime)) - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.white) - - if let endReason = session.endReason { - HStack(spacing: Design.Spacing.xSmall) { - Image(systemName: endReason == .brokeOut ? "xmark.circle.fill" : "checkmark.circle.fill") - .foregroundStyle(endReason == .brokeOut ? .red : .green) - Text(endReason == .brokeOut ? String(localized: "Ran out of chips") : String(localized: "Ended manually")) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .font(.system(size: Design.BaseFontSize.small)) - } - } - - Spacer() - - // Net result badge - VStack(alignment: .trailing, spacing: Design.Spacing.xxSmall) { - Text(SessionFormatter.formatMoney(session.netResult)) - .font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded)) - .foregroundStyle(session.netResult >= 0 ? .green : .red) - - Text(SessionFormatter.formatPercent(session.winRate) + " " + String(localized: "win rate")) - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - } - } - .padding() - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(Color.Sheet.sectionFill) - ) - .padding(.horizontal) - } -} - -// MARK: - Local Size Constants - -private enum Size { - static let outcomeCircleSize: CGFloat = 48 - static let outcomeCircleInner: CGFloat = 24 - static let statIconWidth: CGFloat = 32 - static let chipIconSize: CGFloat = 28 } // MARK: - Preview diff --git a/CasinoKit/Sources/CasinoKit/Exports.swift b/CasinoKit/Sources/CasinoKit/Exports.swift index 580ecac..086a44b 100644 --- a/CasinoKit/Sources/CasinoKit/Exports.swift +++ b/CasinoKit/Sources/CasinoKit/Exports.swift @@ -15,6 +15,8 @@ // - TableLimits // - OnboardingState // - TooltipManager, TooltipConfig +// - GameSettingsProtocol (shared settings interface) +// - SettingsKeys, SettingsDefaults (persistence helpers) // MARK: - Views // - CardView, CardFrontView, CardBackView, CardPlaceholderView @@ -71,6 +73,7 @@ // - SelectionIndicator (checkmark circle) // - BadgePill (capsule badge for values) + // MARK: - Branding // - AppIconView, AppIconConfig // - LaunchScreenView, LaunchScreenConfig, StaticLaunchScreenView @@ -95,6 +98,7 @@ // - GameSession (generic session with game-specific stats) // - GameSpecificStats (protocol for game-specific statistics) // - SessionManagedGame (protocol for games with session management) +// - CasinoGameState (protocol extending SessionManagedGame with shared behaviors) // - SessionEndReason (.manualEnd, .brokeOut) // - RoundOutcome (.win, .lose, .push) // - AggregatedSessionStats (combined stats from multiple sessions) @@ -104,6 +108,21 @@ // - CurrentSessionHeader, SessionSummaryRow (UI components) // - GameStatRow (display a stat item) +// MARK: - Statistics Components +// - StatisticsTab (enum: current, global, history) +// - StatisticsTabSelector (tab picker for stats views) +// - StatColumn (vertical stat display) +// - OutcomeCircle (win/loss/push circles) +// - StatRow (horizontal stat with icon) +// - ChipStatRow (stat with colored circle icon) +// - NoActiveSessionView (placeholder) +// - EmptyHistoryView (placeholder) +// - SessionPerformanceSection (winning/losing sessions) +// - ChipsStatsSection (betting stats) +// - BalanceSection (starting/ending balance) +// - SessionDetailHeader (date, result, win rate) +// - DeleteSessionButton (destructive delete button) + // MARK: - Debug // - debugBorder(_:color:label:) View modifier diff --git a/CasinoKit/Sources/CasinoKit/Models/Card.swift b/CasinoKit/Sources/CasinoKit/Models/Card.swift index bc9e75d..a016544 100644 --- a/CasinoKit/Sources/CasinoKit/Models/Card.swift +++ b/CasinoKit/Sources/CasinoKit/Models/Card.swift @@ -70,26 +70,8 @@ public enum Rank: Int, CaseIterable, Identifiable, Sendable { } } - /// The baccarat point value (Ace = 1, 2-9 = face value, 10/J/Q/K = 0). - public var baccaratValue: Int { - switch self { - case .ace: return 1 - case .two, .three, .four, .five, .six, .seven, .eight, .nine: - return rawValue - case .ten, .jack, .queen, .king: - return 0 - } - } - - /// The blackjack value (Ace = 1 or 11, 2-10 = face value, J/Q/K = 10). - /// Note: Ace flexibility (1 or 11) should be handled by game logic. - public var blackjackValue: Int { - switch self { - case .ace: return 11 // Game logic should handle soft/hard hands - case .jack, .queen, .king: return 10 - default: return rawValue - } - } + // Game-specific value properties (baccaratValue, blackjackValue) should be + // defined as extensions in the respective game apps. /// Accessibility name for VoiceOver. public var accessibilityName: String { @@ -123,15 +105,7 @@ public struct Card: Identifiable, Equatable, Sendable { self.rank = rank } - /// The baccarat point value of this card. - public var baccaratValue: Int { - rank.baccaratValue - } - - /// The blackjack value of this card. - public var blackjackValue: Int { - rank.blackjackValue - } + // Game-specific value properties should be defined as extensions in game apps. /// Display string showing rank and suit together. public var display: String { diff --git a/CasinoKit/Sources/CasinoKit/Models/CasinoGameState.swift b/CasinoKit/Sources/CasinoKit/Models/CasinoGameState.swift new file mode 100644 index 0000000..2ce1fe6 --- /dev/null +++ b/CasinoKit/Sources/CasinoKit/Models/CasinoGameState.swift @@ -0,0 +1,128 @@ +// +// CasinoGameState.swift +// CasinoKit +// +// Protocol defining common state management patterns for casino games. +// Extends SessionManagedGame with additional shared functionality. +// + +import Foundation + +/// Protocol for casino game state classes that manage game flow, +/// session tracking, and common behaviors. +/// +/// This extends `SessionManagedGame` to add additional requirements +/// that are common across all casino games in this app. +@MainActor +public protocol CasinoGameState: SessionManagedGame { + /// The settings object conforming to GameSettingsProtocol. + associatedtype GameSettingsType: GameSettingsProtocol + + /// Game-specific settings. + var settings: GameSettingsType { get } + + /// The shared sound manager. + var soundManager: SoundManager { get } + + /// Onboarding state for first-time users. + var onboarding: OnboardingState { get } + + /// Resets the game to starting conditions (balance, cards, etc.) + /// without clearing session history. + func resetGame() + + /// Clears all data including session history. + func clearAllData() + + /// Aggregated statistics from all sessions. + var aggregatedStats: AggregatedSessionStats { get } +} + +// MARK: - Default Implementations + +public extension CasinoGameState { + /// Default implementation uses shared SoundManager. + var soundManager: SoundManager { SoundManager.shared } + + /// Default aggregated stats computed from session history. + var aggregatedStats: AggregatedSessionStats { + sessionHistory.aggregatedStats() + } + + /// Play a sound effect if enabled. + func playSound(_ sound: GameSound) { + soundManager.play(sound) + } + + /// Play light haptic feedback if enabled. + func hapticLight() { + soundManager.hapticLight() + } + + /// Play medium haptic feedback if enabled. + func hapticMedium() { + soundManager.hapticMedium() + } + + /// Play success haptic feedback if enabled. + func hapticSuccess() { + soundManager.hapticSuccess() + } + + /// Play error haptic feedback if enabled. + func hapticError() { + soundManager.hapticError() + } + + /// Convenience: Check if we should play sounds. + var isSoundEnabled: Bool { + settings.soundEnabled && soundManager.soundEnabled + } + + /// Convenience: Check if we should play haptics. + var isHapticsEnabled: Bool { + settings.hapticsEnabled && soundManager.hapticsEnabled + } +} + +// MARK: - Balance Management Extensions + +public extension CasinoGameState { + /// Whether the player is out of chips (balance is 0 or below minimum bet). + var isBroke: Bool { + balance < settings.minBet + } + + /// Whether the player can afford a bet of the given amount. + func canAffordBet(_ amount: Int) -> Bool { + balance >= amount + } + + /// The maximum bet the player can currently place. + var maxAffordableBet: Int { + min(balance, settings.maxBet) + } +} + +// MARK: - Game Flow Extensions + +public extension CasinoGameState { + /// Ends the current session due to running out of chips. + func endSessionDueToBroke() { + if currentSession != nil { + endCurrentSession(reason: .brokeOut) + } + } + + /// Checks if the player is broke and handles session ending if needed. + /// Returns true if the player is broke. + @discardableResult + func checkBrokeStatus() -> Bool { + if isBroke { + endSessionDueToBroke() + return true + } + return false + } +} + diff --git a/CasinoKit/Sources/CasinoKit/Models/GameSettingsProtocol.swift b/CasinoKit/Sources/CasinoKit/Models/GameSettingsProtocol.swift new file mode 100644 index 0000000..621e7d1 --- /dev/null +++ b/CasinoKit/Sources/CasinoKit/Models/GameSettingsProtocol.swift @@ -0,0 +1,106 @@ +// +// GameSettingsProtocol.swift +// CasinoKit +// +// Protocol defining common settings shared across all casino games. +// Each game can conform to this protocol and add game-specific settings. +// + +import Foundation + +/// Protocol defining the minimum required settings for any casino game. +/// Games conforming to this protocol share common UI settings and behaviors. +@MainActor +public protocol GameSettingsProtocol: AnyObject { + // MARK: - Table Configuration + + /// The table limits preset (affects min/max bets). + var tableLimits: TableLimits { get set } + + /// Minimum bet amount (derived from tableLimits). + var minBet: Int { get } + + /// Maximum bet amount (derived from tableLimits). + var maxBet: Int { get } + + /// Starting balance for new sessions. + var startingBalance: Int { get set } + + // MARK: - Animation Settings + + /// Whether to show card dealing and other animations. + var showAnimations: Bool { get set } + + /// Speed multiplier for dealing (1.0 = normal). + var dealingSpeed: Double { get set } + + // MARK: - Display Settings + + /// Whether to show hints and recommendations. + var showHints: Bool { get set } + + // MARK: - Sound Settings + + /// Whether sound effects are enabled. + var soundEnabled: Bool { get set } + + /// Whether haptic feedback is enabled. + var hapticsEnabled: Bool { get set } + + /// Volume level for sound effects (0.0 to 1.0). + var soundVolume: Float { get set } + + // MARK: - Persistence + + /// Saves settings to persistent storage. + func save() + + /// Loads settings from persistent storage. + func load() + + /// Resets all settings to defaults. + func resetToDefaults() +} + +// MARK: - Default Implementations + +public extension GameSettingsProtocol { + /// Minimum bet derived from table limits. + var minBet: Int { + tableLimits.minBet + } + + /// Maximum bet derived from table limits. + var maxBet: Int { + tableLimits.maxBet + } +} + +// MARK: - Settings Persistence Helpers + +/// Common settings keys for persistence. +public enum SettingsKeys { + public static let tableLimits = "settings.tableLimits" + public static let startingBalance = "settings.startingBalance" + public static let showAnimations = "settings.showAnimations" + public static let dealingSpeed = "settings.dealingSpeed" + public static let showHints = "settings.showHints" + public static let soundEnabled = "settings.soundEnabled" + public static let hapticsEnabled = "settings.hapticsEnabled" + public static let soundVolume = "settings.soundVolume" +} + +// MARK: - Settings Defaults + +/// Default values for common settings. +public enum SettingsDefaults { + public static let tableLimits: TableLimits = .casual + public static let startingBalance: Int = 1_000 + public static let showAnimations: Bool = true + public static let dealingSpeed: Double = 1.0 + public static let showHints: Bool = true + public static let soundEnabled: Bool = true + public static let hapticsEnabled: Bool = true + public static let soundVolume: Float = 1.0 +} + diff --git a/CasinoKit/Sources/CasinoKit/Resources/Localizable.xcstrings b/CasinoKit/Sources/CasinoKit/Resources/Localizable.xcstrings index 38fab6a..82f145e 100644 --- a/CasinoKit/Sources/CasinoKit/Resources/Localizable.xcstrings +++ b/CasinoKit/Sources/CasinoKit/Resources/Localizable.xcstrings @@ -217,6 +217,10 @@ } } }, + "$%lld" : { + "comment" : "A label displaying the starting balance for a session. The value inside the parentheses is replaced with the actual starting balance.", + "isCommentAutoGenerated" : true + }, "$%lld bet" : { "comment" : "A value describing the bet amount in the accessibility label. The argument is the bet amount.", "extractionState" : "stale", @@ -431,6 +435,10 @@ } } }, + "Average bet" : { + "comment" : "Label for the average bet value in the ChipsStatsSection.", + "isCommentAutoGenerated" : true + }, "Balance" : { "localizations" : { "en" : { @@ -453,6 +461,14 @@ } } }, + "Best gain" : { + "comment" : "Label in the \"Chips Stats Section\" view for the user's best gain.", + "isCommentAutoGenerated" : true + }, + "Best session" : { + "comment" : "A label describing the best session amount in the statistics view.", + "isCommentAutoGenerated" : true + }, "Betting disabled" : { "comment" : "A hint that appears when a betting zone is disabled.", "extractionState" : "stale", @@ -482,6 +498,10 @@ "comment" : "The accessibility label for the betting hint view.", "isCommentAutoGenerated" : true }, + "Biggest bet" : { + "comment" : "Label for the \"Biggest bet\" statistic in the ChipsStatsSection.", + "isCommentAutoGenerated" : true + }, "Bust" : { "comment" : "A string describing when a player busts out of a game.", "isCommentAutoGenerated" : true @@ -649,6 +669,10 @@ } } }, + "Completed sessions will appear here." : { + "comment" : "A description of what to expect to see in the completed sessions section of the statistics view.", + "isCommentAutoGenerated" : true + }, "Contact Us" : { "localizations" : { "en" : { @@ -671,6 +695,10 @@ } } }, + "Current" : { + "comment" : "Title of the \"Current\" tab in the statistics view.", + "isCommentAutoGenerated" : true + }, "Current Session" : { "comment" : "A label for the header of the current session section.", "isCommentAutoGenerated" : true @@ -748,6 +776,10 @@ } } }, + "Delete Session" : { + "comment" : "A button label that deletes a session.", + "isCommentAutoGenerated" : true + }, "Diamonds" : { "localizations" : { "en" : { @@ -899,6 +931,14 @@ "comment" : "A label indicating that a session has ended.", "isCommentAutoGenerated" : true }, + "Ended manually" : { + "comment" : "A description of how a session ended manually.", + "isCommentAutoGenerated" : true + }, + "Ending balance" : { + "comment" : "A label describing the user's balance at the end of a session.", + "isCommentAutoGenerated" : true + }, "Exclusive VIP room" : { "localizations" : { "en" : { @@ -1059,6 +1099,10 @@ } } }, + "Global" : { + "comment" : "Title for the \"Global\" tab in the statistics view.", + "isCommentAutoGenerated" : true + }, "Got it" : { }, @@ -1132,6 +1176,10 @@ } } }, + "History" : { + "comment" : "Title of the History tab in the Statistics sheet.", + "isCommentAutoGenerated" : true + }, "iCloud Sync" : { "localizations" : { "en" : { @@ -1374,6 +1422,10 @@ } } }, + "Losing sessions" : { + "comment" : "A label for the number of sessions a user has lost.", + "isCommentAutoGenerated" : true + }, "Low Stakes" : { "localizations" : { "en" : { @@ -1495,6 +1547,10 @@ } } }, + "No Active Session" : { + "comment" : "A label describing a state where there is no active session.", + "isCommentAutoGenerated" : true + }, "No bet" : { "comment" : "A description of a zone with no active bet.", "extractionState" : "stale", @@ -1519,6 +1575,9 @@ } } } + }, + "No Session History" : { + }, "Our apps do not integrate with third-party services that collect user data. We do not share any information with third parties." : { "localizations" : { @@ -1674,6 +1733,10 @@ } } }, + "Ran out of chips" : { + "comment" : "A description of why a casino session might have ended with a negative net result.", + "isCommentAutoGenerated" : true + }, "Regular casino table" : { "localizations" : { "en" : { @@ -1924,6 +1987,14 @@ }, "Start Playing" : { + }, + "Start playing to begin tracking your session." : { + "comment" : "A description below the title of the view, explaining that users can start playing to track their sessions.", + "isCommentAutoGenerated" : true + }, + "Starting balance" : { + "comment" : "A label describing the starting balance of a session.", + "isCommentAutoGenerated" : true }, "Statistics" : { "localizations" : { @@ -2039,6 +2110,14 @@ "comment" : "A label displayed alongside the total winnings in the result banner.", "isCommentAutoGenerated" : true }, + "Total bet" : { + "comment" : "Label for the total amount bet in a statistics view.", + "isCommentAutoGenerated" : true + }, + "Total gain" : { + "comment" : "Label for the \"Total gain\" row in the chips statistics section.", + "isCommentAutoGenerated" : true + }, "Total loss: %lld" : { "comment" : "A string that describes the total loss in a casino game. The argument is the absolute value of the total loss, formatted in U.S. Dollars.", "isCommentAutoGenerated" : true @@ -2249,6 +2328,22 @@ } } }, + "win rate" : { + "comment" : "A label describing the win rate of a session.", + "isCommentAutoGenerated" : true + }, + "Winning sessions" : { + "comment" : "A label describing the number of sessions the user has won.", + "isCommentAutoGenerated" : true + }, + "Worst loss" : { + "comment" : "Label for the worst loss in the statistics section.", + "isCommentAutoGenerated" : true + }, + "Worst session" : { + "comment" : "A label for the worst session amount in the statistics view.", + "isCommentAutoGenerated" : true + }, "You can disable iCloud sync at any time in the app settings" : { "comment" : "Text in the Privacy Policy View explaining how to disable iCloud sync in the app settings.", "isCommentAutoGenerated" : true, diff --git a/CasinoKit/Sources/CasinoKit/Views/Branding/AppIconView.swift b/CasinoKit/Sources/CasinoKit/Views/Branding/AppIconView.swift index c5afd81..091a9b9 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Branding/AppIconView.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Branding/AppIconView.swift @@ -33,31 +33,17 @@ public struct AppIconConfig: Sendable { self.accentColor = accentColor } - // MARK: - Preset Configurations + // MARK: - Example Preset Configurations + // Game-specific presets should be defined in the respective apps as extensions. - /// Baccarat game icon configuration. - public static let baccarat = AppIconConfig( - title: "BACCARAT", - iconSymbol: "suit.spade.fill" - ) - - /// Blackjack game icon configuration. - public static let blackjack = AppIconConfig( - title: "BLACKJACK", - subtitle: "21", - iconSymbol: "suit.club.fill", - primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15), - secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1) - ) - - /// Poker game icon configuration. + /// Poker game icon configuration (example preset). public static let poker = AppIconConfig( title: "POKER", iconSymbol: "suit.diamond.fill", accentColor: .red ) - /// Roulette game icon configuration. + /// Roulette game icon configuration (example preset). public static let roulette = AppIconConfig( title: "ROULETTE", iconSymbol: "circle.grid.3x3.fill", @@ -195,22 +181,20 @@ private struct DiamondPatternOverlay: View { // MARK: - Preview -#Preview("Baccarat Icon") { - AppIconView(config: .baccarat, size: 512) +#Preview("Poker Icon") { + AppIconView(config: .poker, size: 512) .padding() .background(Color.gray) } -#Preview("Blackjack Icon") { - AppIconView(config: .blackjack, size: 512) +#Preview("Roulette Icon") { + AppIconView(config: .roulette, size: 512) .padding() .background(Color.gray) } #Preview("All Icons") { HStack(spacing: 20) { - AppIconView(config: .baccarat, size: 200) - AppIconView(config: .blackjack, size: 200) AppIconView(config: .poker, size: 200) AppIconView(config: .roulette, size: 200) } diff --git a/CasinoKit/Sources/CasinoKit/Views/Branding/IconRenderer.swift b/CasinoKit/Sources/CasinoKit/Views/Branding/IconRenderer.swift index b746a75..59f5de7 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Branding/IconRenderer.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Branding/IconRenderer.swift @@ -134,6 +134,6 @@ public struct IconExportView: View { } #Preview("Icon Export") { - IconExportView(config: .baccarat) + IconExportView(config: .poker) } diff --git a/CasinoKit/Sources/CasinoKit/Views/Branding/LaunchScreenView.swift b/CasinoKit/Sources/CasinoKit/Views/Branding/LaunchScreenView.swift index 6f675be..8c44758 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Branding/LaunchScreenView.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Branding/LaunchScreenView.swift @@ -38,26 +38,10 @@ public struct LaunchScreenConfig: Sendable { self.showLoadingIndicator = showLoadingIndicator } - // MARK: - Preset Configurations + // MARK: - Example Preset Configurations + // Game-specific presets should be defined in the respective apps as extensions. - /// Baccarat game launch screen configuration. - public static let baccarat = LaunchScreenConfig( - title: "BACCARAT", - tagline: "The Classic Casino Card Game", - iconSymbols: ["suit.spade.fill", "suit.heart.fill"] - ) - - /// Blackjack game launch screen configuration. - public static let blackjack = LaunchScreenConfig( - title: "BLACKJACK", - subtitle: "21", - tagline: "Beat the Dealer", - iconSymbols: ["suit.club.fill", "suit.diamond.fill"], - primaryColor: Color(red: 0.05, green: 0.35, blue: 0.15), - secondaryColor: Color(red: 0.03, green: 0.2, blue: 0.1) - ) - - /// Poker game launch screen configuration. + /// Poker game launch screen configuration (example preset). public static let poker = LaunchScreenConfig( title: "POKER", tagline: "Texas Hold'em", @@ -339,15 +323,11 @@ public struct StaticLaunchScreenView: View { // MARK: - Preview -#Preview("Baccarat Launch") { - LaunchScreenView(config: .baccarat) -} - -#Preview("Blackjack Launch") { - LaunchScreenView(config: .blackjack) +#Preview("Poker Launch") { + LaunchScreenView(config: .poker) } #Preview("Static Launch") { - StaticLaunchScreenView(config: .baccarat) + StaticLaunchScreenView(config: .poker) } diff --git a/CasinoKit/Sources/CasinoKit/Views/Buttons/ActionButton.swift b/CasinoKit/Sources/CasinoKit/Views/Buttons/ActionButton.swift index d234a54..19afc55 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Buttons/ActionButton.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Buttons/ActionButton.swift @@ -142,7 +142,7 @@ public enum ActionButtonStyle { } } -#Preview("BlackJack Action Buttons") { +#Preview("Casino Action Buttons") { ZStack { Color.CasinoTable.felt.ignoresSafeArea() diff --git a/CasinoKit/Sources/CasinoKit/Views/Overlays/GameOverView.swift b/CasinoKit/Sources/CasinoKit/Views/Overlays/GameOverView.swift index 78711cf..5e4e4bf 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Overlays/GameOverView.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Overlays/GameOverView.swift @@ -76,14 +76,14 @@ public struct GameOverView: View { // Stats card VStack(spacing: CasinoDesign.Spacing.medium) { - StatRow( + GameOverStatRow( label: String(localized: "Rounds Played", bundle: .module), value: "\(roundsPlayed)", fontSize: statsFontSize ) ForEach(additionalStats.indices, id: \.self) { index in - StatRow( + GameOverStatRow( label: additionalStats[index].0, value: additionalStats[index].1, fontSize: statsFontSize @@ -170,7 +170,7 @@ public struct GameOverView: View { } /// A single stat row for the game over view. -private struct StatRow: View { +private struct GameOverStatRow: View { let label: String let value: String let fontSize: CGFloat diff --git a/CasinoKit/Sources/CasinoKit/Views/Session/SessionViews.swift b/CasinoKit/Sources/CasinoKit/Views/Session/SessionViews.swift index 0cc2e01..297dab7 100644 --- a/CasinoKit/Sources/CasinoKit/Views/Session/SessionViews.swift +++ b/CasinoKit/Sources/CasinoKit/Views/Session/SessionViews.swift @@ -332,25 +332,8 @@ public struct CurrentSessionHeader: View { } } -// MARK: - Stat Column (Helper) - -private struct StatColumn: View { - let value: String - let label: String - var valueColor: Color = .white - - var body: some View { - VStack(spacing: CasinoDesign.Spacing.xSmall) { - Text(value) - .font(.system(size: CasinoDesign.BaseFontSize.large, weight: .bold, design: .rounded)) - .foregroundStyle(valueColor) - Text(label) - .font(.system(size: CasinoDesign.BaseFontSize.small)) - .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) - } - .frame(maxWidth: .infinity) - } -} +// MARK: - Stat Column +// StatColumn is now provided by StatisticsComponents.swift // MARK: - Game Stats Display Row diff --git a/CasinoKit/Sources/CasinoKit/Views/Session/StatisticsComponents.swift b/CasinoKit/Sources/CasinoKit/Views/Session/StatisticsComponents.swift new file mode 100644 index 0000000..71b2815 --- /dev/null +++ b/CasinoKit/Sources/CasinoKit/Views/Session/StatisticsComponents.swift @@ -0,0 +1,581 @@ +// +// StatisticsComponents.swift +// CasinoKit +// +// Reusable UI components for statistics display. +// Used in StatisticsSheetView across all casino games. +// + +import SwiftUI + +// MARK: - Statistics Tab + +/// Tab options for statistics views. +public enum StatisticsTab: CaseIterable, Sendable { + case current + case global + case history + + public var title: String { + switch self { + case .current: return String(localized: "Current", bundle: .module) + case .global: return String(localized: "Global", bundle: .module) + case .history: return String(localized: "History", bundle: .module) + } + } + + public var icon: String { + switch self { + case .current: return "play.circle.fill" + case .global: return "globe" + case .history: return "clock.arrow.circlepath" + } + } +} + +// MARK: - Statistics Tab Selector + +/// A tab selector for switching between Current, Global, and History tabs. +public struct StatisticsTabSelector: View { + @Binding public var selectedTab: StatisticsTab + + public init(selectedTab: Binding) { + self._selectedTab = selectedTab + } + + public var body: some View { + HStack(spacing: 0) { + ForEach(StatisticsTab.allCases, id: \.self) { tab in + Button { + withAnimation(.spring(duration: CasinoDesign.Animation.springDuration)) { + selectedTab = tab + } + } label: { + VStack(spacing: CasinoDesign.Spacing.xSmall) { + Image(systemName: tab.icon) + .font(.system(size: CasinoDesign.BaseFontSize.large)) + Text(tab.title) + .font(.system(size: CasinoDesign.BaseFontSize.small, weight: .medium)) + } + .foregroundStyle(selectedTab == tab ? Color.Sheet.accent : .white.opacity(CasinoDesign.Opacity.medium)) + .frame(maxWidth: .infinity) + .padding(.vertical, CasinoDesign.Spacing.medium) + .background( + RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium) + .fill(selectedTab == tab ? Color.Sheet.accent.opacity(CasinoDesign.Opacity.hint) : .clear) + ) + } + } + } + .padding(.horizontal) + } +} + +// MARK: - Stat Column + +/// A vertical column displaying a value and label, used in summary sections. +public struct StatColumn: View { + public let value: String + public let label: String + public var valueColor: Color + + public init(value: String, label: String, valueColor: Color = .white) { + self.value = value + self.label = label + self.valueColor = valueColor + } + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.xSmall) { + Text(value) + .font(.system(size: CasinoDesign.BaseFontSize.large, weight: .bold, design: .rounded)) + .foregroundStyle(valueColor) + Text(label) + .font(.system(size: CasinoDesign.BaseFontSize.small)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) + } + .frame(maxWidth: .infinity) + } +} + +// MARK: - Outcome Circle + +/// A circular indicator showing win/loss/push counts with a colored ring. +public struct OutcomeCircle: View { + public let label: String + public let count: Int + public let color: Color + + // Local size constants + private let circleSize: CGFloat = 48 + private let innerSize: CGFloat = 24 + + public init(label: String, count: Int, color: Color) { + self.label = label + self.count = count + self.color = color + } + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.small) { + Text(label) + .font(.system(size: CasinoDesign.BaseFontSize.small)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) + + Text("\(count)") + .font(.system(size: CasinoDesign.BaseFontSize.large, weight: .bold)) + .foregroundStyle(.white) + + ZStack { + Circle() + .fill(Color.black.opacity(CasinoDesign.Opacity.light)) + .frame(width: circleSize, height: circleSize) + + Circle() + .stroke(color, lineWidth: CasinoDesign.LineWidth.thick) + .frame(width: circleSize, height: circleSize) + + Circle() + .fill(color.opacity(CasinoDesign.Opacity.medium)) + .frame(width: innerSize, height: innerSize) + } + } + .frame(maxWidth: .infinity) + } +} + +// MARK: - Stat Row + +/// A horizontal row with icon, label, and value. +public struct StatRow: View { + public let icon: String + public let label: String + public let value: String + public var valueColor: Color + + // Local size constants + private let iconWidth: CGFloat = 32 + + public init(icon: String, label: String, value: String, valueColor: Color = .white) { + self.icon = icon + self.label = label + self.value = value + self.valueColor = valueColor + } + + public var body: some View { + HStack { + Image(systemName: icon) + .font(.system(size: CasinoDesign.BaseFontSize.large)) + .foregroundStyle(Color.Sheet.accent) + .frame(width: iconWidth) + + Text(label) + .font(.system(size: CasinoDesign.BaseFontSize.body)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.strong)) + + Spacer() + + Text(value) + .font(.system(size: CasinoDesign.BaseFontSize.body, weight: .semibold, design: .rounded)) + .foregroundStyle(valueColor) + } + } +} + +// MARK: - Chip Stat Row + +/// A row displaying a stat with a colored circular icon, commonly used for chip/money stats. +public struct ChipStatRow: View { + public let icon: String + public let iconColor: Color + public let label: String + public let value: String + + // Local size constants + private let chipIconSize: CGFloat = 28 + + public init(icon: String, iconColor: Color, label: String, value: String) { + self.icon = icon + self.iconColor = iconColor + self.label = label + self.value = value + } + + public var body: some View { + HStack { + ZStack { + Circle() + .fill(iconColor) + .frame(width: chipIconSize, height: chipIconSize) + + Image(systemName: icon) + .font(.system(size: CasinoDesign.BaseFontSize.small, weight: .bold)) + .foregroundStyle(.white) + } + + Text(label) + .font(.system(size: CasinoDesign.BaseFontSize.body)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.strong)) + + Spacer() + + Text(value) + .font(.system(size: CasinoDesign.BaseFontSize.body, weight: .semibold, design: .rounded)) + .foregroundStyle(Color.Sheet.accent) + } + } +} + +// MARK: - No Active Session View + +/// Placeholder view shown when there's no active session. +public struct NoActiveSessionView: View { + public init() {} + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.large) { + Image(systemName: "play.slash") + .font(.system(size: CasinoDesign.BaseFontSize.xxLarge * 2)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.light)) + + Text(String(localized: "No Active Session", bundle: .module)) + .font(.system(size: CasinoDesign.BaseFontSize.large, weight: .semibold)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) + + Text(String(localized: "Start playing to begin tracking your session.", bundle: .module)) + .font(.system(size: CasinoDesign.BaseFontSize.body)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.light)) + .multilineTextAlignment(.center) + } + .padding(CasinoDesign.Spacing.xxLarge) + } +} + +// MARK: - Empty History View + +/// Placeholder view shown when session history is empty. +public struct EmptyHistoryView: View { + public init() {} + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.large) { + Image(systemName: "clock.arrow.circlepath") + .font(.system(size: CasinoDesign.BaseFontSize.xxLarge * 2)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.light)) + + Text(String(localized: "No Session History", bundle: .module)) + .font(.system(size: CasinoDesign.BaseFontSize.large, weight: .semibold)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) + + Text(String(localized: "Completed sessions will appear here.", bundle: .module)) + .font(.system(size: CasinoDesign.BaseFontSize.body)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.light)) + .multilineTextAlignment(.center) + } + .padding(CasinoDesign.Spacing.xxLarge) + } +} + +// MARK: - Session Performance Section + +/// A reusable section showing session win/loss performance stats. +public struct SessionPerformanceSection: View { + public let winningSessions: Int + public let losingSessions: Int + public let bestSession: Int + public let worstSession: Int + + public init( + winningSessions: Int, + losingSessions: Int, + bestSession: Int, + worstSession: Int + ) { + self.winningSessions = winningSessions + self.losingSessions = losingSessions + self.bestSession = bestSession + self.worstSession = worstSession + } + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.medium) { + HStack { + Text(String(localized: "Winning sessions", bundle: .module)) + Spacer() + Text("\(winningSessions)") + .foregroundStyle(.green) + .bold() + } + HStack { + Text(String(localized: "Losing sessions", bundle: .module)) + Spacer() + Text("\(losingSessions)") + .foregroundStyle(.red) + .bold() + } + + Divider().background(Color.white.opacity(CasinoDesign.Opacity.hint)) + + HStack { + Text(String(localized: "Best session", bundle: .module)) + Spacer() + Text(SessionFormatter.formatMoney(bestSession)) + .foregroundStyle(.green) + .bold() + } + HStack { + Text(String(localized: "Worst session", bundle: .module)) + Spacer() + Text(SessionFormatter.formatMoney(worstSession)) + .foregroundStyle(.red) + .bold() + } + } + .font(.system(size: CasinoDesign.BaseFontSize.body)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.strong)) + } +} + +// MARK: - Chips Stats Section + +/// A reusable section showing chip/betting statistics. +public struct ChipsStatsSection: View { + public let totalWinnings: Int + public let biggestWin: Int + public let biggestLoss: Int + public let totalBetAmount: Int + public let averageBet: Int? + public let biggestBet: Int + + public init( + totalWinnings: Int, + biggestWin: Int, + biggestLoss: Int, + totalBetAmount: Int, + averageBet: Int?, + biggestBet: Int + ) { + self.totalWinnings = totalWinnings + self.biggestWin = biggestWin + self.biggestLoss = biggestLoss + self.totalBetAmount = totalBetAmount + self.averageBet = averageBet + self.biggestBet = biggestBet + } + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.medium) { + ChipStatRow( + icon: "chart.line.uptrend.xyaxis", + iconColor: totalWinnings >= 0 ? .green : .red, + label: String(localized: "Total gain", bundle: .module), + value: SessionFormatter.formatMoney(totalWinnings) + ) + + ChipStatRow( + icon: "arrow.up.circle.fill", + iconColor: .green, + label: String(localized: "Best gain", bundle: .module), + value: SessionFormatter.formatMoney(biggestWin) + ) + + ChipStatRow( + icon: "arrow.down.circle.fill", + iconColor: .red, + label: String(localized: "Worst loss", bundle: .module), + value: SessionFormatter.formatMoney(biggestLoss) + ) + + Divider().background(Color.white.opacity(CasinoDesign.Opacity.hint)) + + ChipStatRow( + icon: "plusminus.circle.fill", + iconColor: .blue, + label: String(localized: "Total bet", bundle: .module), + value: "$\(totalBetAmount)" + ) + + if let avg = averageBet { + ChipStatRow( + icon: "equal.circle.fill", + iconColor: .purple, + label: String(localized: "Average bet", bundle: .module), + value: "$\(avg)" + ) + } + + ChipStatRow( + icon: "star.circle.fill", + iconColor: .orange, + label: String(localized: "Biggest bet", bundle: .module), + value: "$\(biggestBet)" + ) + } + } +} + +// MARK: - Balance Section + +/// A reusable section showing starting/ending balance for a session. +public struct BalanceSection: View { + public let startingBalance: Int + public let endingBalance: Int + public let netResult: Int + + public init(startingBalance: Int, endingBalance: Int, netResult: Int) { + self.startingBalance = startingBalance + self.endingBalance = endingBalance + self.netResult = netResult + } + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.medium) { + HStack { + Text(String(localized: "Starting balance", bundle: .module)) + Spacer() + Text("$\(startingBalance)") + .bold() + } + HStack { + Text(String(localized: "Ending balance", bundle: .module)) + Spacer() + Text("$\(endingBalance)") + .foregroundStyle(netResult >= 0 ? .green : .red) + .bold() + } + } + .font(.system(size: CasinoDesign.BaseFontSize.body)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.strong)) + } +} + +// MARK: - Session Header + +/// Header showing session date, end reason, net result, and win rate. +public struct SessionDetailHeader: View { + public let startTime: Date + public let endReason: SessionEndReason? + public let netResult: Int + public let winRate: Double + + public init( + startTime: Date, + endReason: SessionEndReason?, + netResult: Int, + winRate: Double + ) { + self.startTime = startTime + self.endReason = endReason + self.netResult = netResult + self.winRate = winRate + } + + public var body: some View { + VStack(spacing: CasinoDesign.Spacing.small) { + HStack { + VStack(alignment: .leading, spacing: CasinoDesign.Spacing.xxSmall) { + Text(SessionFormatter.formatSessionDate(startTime)) + .font(.system(size: CasinoDesign.BaseFontSize.body, weight: .medium)) + .foregroundStyle(.white) + + if let endReason = endReason { + HStack(spacing: CasinoDesign.Spacing.xSmall) { + Image(systemName: endReason == .brokeOut ? "xmark.circle.fill" : "checkmark.circle.fill") + .foregroundStyle(endReason == .brokeOut ? .red : .green) + Text(endReason == .brokeOut + ? String(localized: "Ran out of chips", bundle: .module) + : String(localized: "Ended manually", bundle: .module)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) + } + .font(.system(size: CasinoDesign.BaseFontSize.small)) + } + } + + Spacer() + + VStack(alignment: .trailing, spacing: CasinoDesign.Spacing.xxSmall) { + Text(SessionFormatter.formatMoney(netResult)) + .font(.system(size: CasinoDesign.BaseFontSize.xLarge, weight: .bold, design: .rounded)) + .foregroundStyle(netResult >= 0 ? .green : .red) + + Text(SessionFormatter.formatPercent(winRate) + " " + String(localized: "win rate", bundle: .module)) + .font(.system(size: CasinoDesign.BaseFontSize.small)) + .foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium)) + } + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium) + .fill(Color.Sheet.sectionFill) + ) + .padding(.horizontal) + } +} + +// MARK: - Delete Session Button + +/// A styled button for deleting a session. +public struct DeleteSessionButton: View { + public let action: () -> Void + + public init(action: @escaping () -> Void) { + self.action = action + } + + public var body: some View { + Button(role: .destructive, action: action) { + HStack { + Image(systemName: "trash") + Text(String(localized: "Delete Session", bundle: .module)) + } + .font(.system(size: CasinoDesign.BaseFontSize.body, weight: .medium)) + .frame(maxWidth: .infinity) + .padding(CasinoDesign.Spacing.medium) + .background( + RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium) + .fill(Color.red.opacity(CasinoDesign.Opacity.hint)) + ) + .foregroundStyle(.red) + } + .padding(.horizontal) + .padding(.top, CasinoDesign.Spacing.large) + } +} + +// MARK: - Previews + +#Preview("Statistics Components") { + ScrollView { + VStack(spacing: 24) { + StatisticsTabSelector(selectedTab: .constant(.current)) + + HStack(spacing: CasinoDesign.Spacing.large) { + StatColumn(value: "15", label: "Sessions") + StatColumn(value: "234", label: "Hands") + StatColumn(value: "52.3%", label: "Win Rate", valueColor: .green) + } + .padding() + + HStack(spacing: CasinoDesign.Spacing.medium) { + OutcomeCircle(label: "Won", count: 45, color: .white) + OutcomeCircle(label: "Lost", count: 38, color: .red) + OutcomeCircle(label: "Push", count: 12, color: .gray) + } + .padding() + + VStack(spacing: CasinoDesign.Spacing.medium) { + StatRow(icon: "clock", label: "Total game time", value: "02h 15min") + ChipStatRow(icon: "chart.line.uptrend.xyaxis", iconColor: .green, label: "Total gain", value: "$1,250") + } + .padding() + + NoActiveSessionView() + + EmptyHistoryView() + } + } + .background(Color.Sheet.background) +} + diff --git a/CasinoKit/Sources/CasinoKit/Views/WelcomeSheet.swift b/CasinoKit/Sources/CasinoKit/Views/WelcomeSheet.swift index 0b42134..453b45a 100644 --- a/CasinoKit/Sources/CasinoKit/Views/WelcomeSheet.swift +++ b/CasinoKit/Sources/CasinoKit/Views/WelcomeSheet.swift @@ -10,6 +10,7 @@ import SwiftUI /// Welcome sheet shown on first launch of a game. public struct WelcomeSheet: View { let gameName: String + let gameEmoji: String let features: [WelcomeFeature] let onStartTutorial: () -> Void let onStartPlaying: () -> Void @@ -22,11 +23,13 @@ public struct WelcomeSheet: View { public init( gameName: String, + gameEmoji: String = "🎰", features: [WelcomeFeature], onStartTutorial: @escaping () -> Void, onStartPlaying: @escaping () -> Void ) { self.gameName = gameName + self.gameEmoji = gameEmoji self.features = features self.onStartTutorial = onStartTutorial self.onStartPlaying = onStartPlaying @@ -88,13 +91,6 @@ public struct WelcomeSheet: View { ) } - private var gameEmoji: String { - switch gameName.lowercased() { - case "blackjack": return "🃏" - case "baccarat": return "🎴" - default: return "🎰" - } - } } // MARK: - Feature Row @@ -149,22 +145,23 @@ public struct WelcomeFeature: Identifiable { #Preview { WelcomeSheet( - gameName: "Blackjack", + gameName: "Casino Game", + gameEmoji: "🎰", features: [ WelcomeFeature( icon: "target", - title: "Beat the Dealer", - description: "Get closer to 21 than the dealer without going over" + title: "Exciting Gameplay", + description: "Experience the thrill of the casino" ), WelcomeFeature( icon: "lightbulb.fill", title: "Learn Strategy", - description: "Built-in hints show optimal plays based on basic strategy" + description: "Built-in hints show optimal plays" ), WelcomeFeature( icon: "dollarsign.circle", title: "Practice Free", - description: "Start with $1,000 and play risk-free" + description: "Start with virtual chips and play risk-free" ) ], onStartTutorial: {}, diff --git a/CasinoKit/Tests/CasinoKitTests/CasinoKitTests.swift b/CasinoKit/Tests/CasinoKitTests/CasinoKitTests.swift index 0f27b49..9ee6044 100644 --- a/CasinoKit/Tests/CasinoKitTests/CasinoKitTests.swift +++ b/CasinoKit/Tests/CasinoKitTests/CasinoKitTests.swift @@ -41,21 +41,8 @@ struct CardTests { #expect(deck.cardsRemaining == 52) } - @Test("Card baccarat values are correct") - func baccaratValues() { - #expect(Rank.ace.baccaratValue == 1) - #expect(Rank.five.baccaratValue == 5) - #expect(Rank.ten.baccaratValue == 0) - #expect(Rank.king.baccaratValue == 0) - } - - @Test("Card blackjack values are correct") - func blackjackValues() { - #expect(Rank.ace.blackjackValue == 11) - #expect(Rank.five.blackjackValue == 5) - #expect(Rank.ten.blackjackValue == 10) - #expect(Rank.king.blackjackValue == 10) - } + // Game-specific card value tests have been moved to the respective app test targets + // (BlackjackTests and BaccaratTests) since the value extensions are now in the apps. @Test("Card display format is correct") func cardDisplay() {