Compare commits
2 Commits
2d07147fa7
...
77f5775cd2
| Author | SHA1 | Date | |
|---|---|---|---|
| 77f5775cd2 | |||
| 1e4e38b7ce |
@ -208,11 +208,12 @@ struct BaccaratEngine {
|
||||
/// - Parameters:
|
||||
/// - bet: The bet that was placed.
|
||||
/// - result: The result of the round.
|
||||
/// - variant: The baccarat variant being played (affects Banker payouts).
|
||||
/// - Returns: The net winnings (positive), net loss (negative), or 0 for push.
|
||||
func calculatePayout(bet: Bet, result: GameResult) -> Int {
|
||||
func calculatePayout(bet: Bet, result: GameResult, variant: BaccaratVariant = .standard) -> Int {
|
||||
switch bet.type {
|
||||
case .player, .banker, .tie:
|
||||
return calculateMainBetPayout(bet: bet, result: result)
|
||||
return calculateMainBetPayout(bet: bet, result: result, variant: variant)
|
||||
|
||||
case .playerPair:
|
||||
return playerHasPair ? Int(Double(bet.amount) * bet.type.payoutMultiplier) : -bet.amount
|
||||
@ -229,15 +230,23 @@ struct BaccaratEngine {
|
||||
}
|
||||
|
||||
/// Calculates payout for main bets (Player, Banker, Tie).
|
||||
private func calculateMainBetPayout(bet: Bet, result: GameResult) -> Int {
|
||||
private func calculateMainBetPayout(bet: Bet, result: GameResult, variant: BaccaratVariant) -> Int {
|
||||
// Standard push logic (e.g., Tie on Player/Banker bets)
|
||||
if result.isPush(for: bet.type) {
|
||||
// Push - bet is returned
|
||||
return 0
|
||||
}
|
||||
|
||||
// Commission-Free: Banker wins with 6 is a push
|
||||
if variant == .commissionFree
|
||||
&& bet.type == .banker
|
||||
&& result == .bankerWins
|
||||
&& bankerHand.value == 6 {
|
||||
return 0 // Push - bet returned
|
||||
}
|
||||
|
||||
if result.isWinningBet(bet.type) {
|
||||
// Win - return winnings based on payout multiplier
|
||||
return Int(Double(bet.amount) * bet.type.payoutMultiplier)
|
||||
// Win - return winnings based on variant-aware payout multiplier
|
||||
return Int(Double(bet.amount) * bet.type.payoutMultiplier(for: variant))
|
||||
} else {
|
||||
// Loss - lose the bet amount
|
||||
return -bet.amount
|
||||
|
||||
@ -938,7 +938,7 @@ final class GameState: CasinoGameState {
|
||||
var dragonBankerWon = false
|
||||
|
||||
for bet in currentBets {
|
||||
let payout = engine.calculatePayout(bet: bet, result: result)
|
||||
let payout = engine.calculatePayout(bet: bet, result: result, variant: settings.gameVariant)
|
||||
totalWinnings += payout
|
||||
|
||||
// Track individual bet result
|
||||
|
||||
@ -45,6 +45,19 @@ enum BetType: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the payout multiplier for this bet type based on the game variant.
|
||||
/// - Parameter variant: The baccarat variant being played.
|
||||
/// - Returns: The payout multiplier (e.g., 1.0 for 1:1, 0.95 for 5% commission).
|
||||
func payoutMultiplier(for variant: BaccaratVariant) -> Double {
|
||||
switch self {
|
||||
case .banker:
|
||||
// Commission-Free pays 1:1 (Banker 6 push handled separately in engine)
|
||||
return variant == .commissionFree ? 1.0 : 0.95
|
||||
default:
|
||||
return payoutMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
/// Display name with payout info.
|
||||
var displayWithPayout: String {
|
||||
switch self {
|
||||
@ -82,6 +95,18 @@ enum BetType: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the payout description for this bet type based on the game variant.
|
||||
/// - Parameter variant: The baccarat variant being played.
|
||||
/// - Returns: The payout description string for display.
|
||||
func payoutDescription(for variant: BaccaratVariant) -> String {
|
||||
switch self {
|
||||
case .banker:
|
||||
return variant == .commissionFree ? "PAYS 1 TO 1" : "PAYS 0.95 TO 1"
|
||||
default:
|
||||
return payoutDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// The color associated with this bet type.
|
||||
var color: Color {
|
||||
switch self {
|
||||
|
||||
@ -67,6 +67,28 @@ enum RevealStyle: String, CaseIterable, Codable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// The baccarat game variant.
|
||||
enum BaccaratVariant: String, CaseIterable, Identifiable, Codable {
|
||||
case standard = "standard"
|
||||
case commissionFree = "commissionFree"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .standard: return String(localized: "Standard (5% Commission)")
|
||||
case .commissionFree: return String(localized: "Commission Free")
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .standard: return String(localized: "Banker pays 0.95:1")
|
||||
case .commissionFree: return String(localized: "Banker pays 1:1, but Banker 6 pushes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TableLimits is now provided by CasinoKit
|
||||
|
||||
/// Observable settings class for Baccarat configuration.
|
||||
@ -129,6 +151,11 @@ final class GameSettings: GameSettingsProtocol {
|
||||
/// Whether to show streak alerts.
|
||||
var showStreakAlerts: Bool = true
|
||||
|
||||
// MARK: - Game Variant Settings
|
||||
|
||||
/// The baccarat game variant (standard vs commission-free).
|
||||
var gameVariant: BaccaratVariant = .standard
|
||||
|
||||
// MARK: - Sound Settings
|
||||
|
||||
/// Whether sound effects are enabled.
|
||||
@ -157,6 +184,7 @@ final class GameSettings: GameSettingsProtocol {
|
||||
static let revealStyle = "settings.revealStyle"
|
||||
static let preferredRoadType = "settings.preferredRoadType"
|
||||
static let showStreakAlerts = "settings.showStreakAlerts"
|
||||
static let gameVariant = "settings.gameVariant"
|
||||
}
|
||||
|
||||
// MARK: - iCloud
|
||||
@ -286,6 +314,11 @@ final class GameSettings: GameSettingsProtocol {
|
||||
if defaults.object(forKey: Keys.showStreakAlerts) != nil {
|
||||
self.showStreakAlerts = defaults.bool(forKey: Keys.showStreakAlerts)
|
||||
}
|
||||
|
||||
if let rawVariant = defaults.string(forKey: Keys.gameVariant),
|
||||
let variant = BaccaratVariant(rawValue: rawVariant) {
|
||||
self.gameVariant = variant
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads settings from iCloud.
|
||||
@ -351,6 +384,11 @@ final class GameSettings: GameSettingsProtocol {
|
||||
if store.object(forKey: Keys.showStreakAlerts) != nil {
|
||||
self.showStreakAlerts = store.bool(forKey: Keys.showStreakAlerts)
|
||||
}
|
||||
|
||||
if let rawVariant = store.string(forKey: Keys.gameVariant),
|
||||
let variant = BaccaratVariant(rawValue: rawVariant) {
|
||||
self.gameVariant = variant
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves settings to UserDefaults and iCloud.
|
||||
@ -371,6 +409,7 @@ final class GameSettings: GameSettingsProtocol {
|
||||
defaults.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
||||
defaults.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
||||
defaults.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
||||
defaults.set(gameVariant.rawValue, forKey: Keys.gameVariant)
|
||||
|
||||
// Also save to iCloud
|
||||
if iCloudAvailable, let store = iCloudStore {
|
||||
@ -388,6 +427,7 @@ final class GameSettings: GameSettingsProtocol {
|
||||
store.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
||||
store.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
||||
store.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
||||
store.set(gameVariant.rawValue, forKey: Keys.gameVariant)
|
||||
store.synchronize()
|
||||
}
|
||||
}
|
||||
@ -426,6 +466,7 @@ final class GameSettings: GameSettingsProtocol {
|
||||
revealStyle = .auto
|
||||
preferredRoadType = .big
|
||||
showStreakAlerts = true
|
||||
gameVariant = .standard
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@ -432,6 +432,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"6 PUSHES" : {
|
||||
"comment" : "A note displayed for Banker 6 pushes in the baccarat betting table.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"8 : 1" : {
|
||||
"comment" : "The payout ratio for a tie bet.",
|
||||
"localizations" : {
|
||||
@ -671,6 +675,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"An alternative to standard Baccarat with simplified payouts." : {
|
||||
|
||||
},
|
||||
"Analyzes patterns from the Big Road." : {
|
||||
"comment" : "Tooltip text for the \"Big Eye Boy\" road type in the road map interface.",
|
||||
@ -1094,6 +1101,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Banker Bet: Pays 1:1 (no 5% commission)" : {
|
||||
|
||||
},
|
||||
"Banker hand" : {
|
||||
"comment" : "A label displayed above the banker's hand.",
|
||||
@ -1141,6 +1151,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Banker pays 0.95:1" : {
|
||||
"comment" : "Description of the baccarat variant where the banker pays 0.95:1.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Banker pays 1:1, but Banker 6 pushes" : {
|
||||
"comment" : "Description of the \"Commission Free\" baccarat variant.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Banker running hot (%lld%%)" : {
|
||||
"comment" : "A hint to place a bet on the Banker based on the calculated house edge. The argument is the percentage of the house edge.",
|
||||
"localizations" : {
|
||||
@ -1257,6 +1275,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Betting Hint" : {
|
||||
"comment" : "An accessibility label for the combined trend and hint view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Betting tips and trend analysis" : {
|
||||
"comment" : "Description for hints feature.",
|
||||
"localizations" : {
|
||||
@ -1722,6 +1744,13 @@
|
||||
"Color Meaning" : {
|
||||
"comment" : "A heading displayed in the color legend of a derived road type popover.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Commission Free" : {
|
||||
"comment" : "Description of a baccarat game variant where the banker always wins, regardless of the player's hand.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Commission-Free Mode" : {
|
||||
|
||||
},
|
||||
"Compares current streak to 2 columns back." : {
|
||||
"comment" : "Tooltip text for the \"Small Road\" road type in the Road Map selector.",
|
||||
@ -2017,6 +2046,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Enable in Settings → Game Variant." : {
|
||||
|
||||
},
|
||||
"End Session" : {
|
||||
"comment" : "The text for a button that ends the current game session.",
|
||||
@ -2086,6 +2118,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"EXCEPT: If Banker wins with a total of 6, it's a push." : {
|
||||
|
||||
},
|
||||
"Finest pattern detection. Looks 3 columns back." : {
|
||||
"comment" : "Short description for a road type that compares the current streak to one from three columns ago.",
|
||||
@ -2159,6 +2194,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GAME VARIANT" : {
|
||||
|
||||
},
|
||||
"Generate & Save Icons" : {
|
||||
"comment" : "A button label that triggers the generation of app icons.",
|
||||
@ -2419,6 +2457,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"House edge is slightly higher (1.46%) but payouts are easier." : {
|
||||
|
||||
},
|
||||
"How to Export Icons" : {
|
||||
"comment" : "A section header explaining how to export app icons.",
|
||||
@ -3460,6 +3501,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Player and Tie bets pay the same as standard Baccarat." : {
|
||||
|
||||
},
|
||||
"Player Bet: Pays 1:1 (even money)" : {
|
||||
"comment" : "Description of the payout for a Player Bet in the Rules Help view.",
|
||||
@ -4447,6 +4491,10 @@
|
||||
"comment" : "Name of the reveal style option that uses pressure-based card peeking.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Standard (5% Commission)" : {
|
||||
"comment" : "Description of the \"Standard\" baccarat variant, including the commission rate.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"STARTING BALANCE" : {
|
||||
"comment" : "Section header for starting balance settings.",
|
||||
"localizations" : {
|
||||
@ -5804,6 +5852,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Your Banker bet is returned — you don't win or lose." : {
|
||||
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
|
||||
@ -333,17 +333,15 @@ struct GameTableView: View, SherpaDelegate {
|
||||
.transition(.opacity)
|
||||
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
||||
|
||||
// Betting hint (static, below table, above chips)
|
||||
if let hintInfo = state.currentHintInfo {
|
||||
BettingHintView(
|
||||
hint: hintInfo.text,
|
||||
secondaryInfo: hintInfo.secondaryText,
|
||||
style: hintInfo.style
|
||||
)
|
||||
.transition(.opacity)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
||||
}
|
||||
// Combined trend and hint view (static, below table, above chips)
|
||||
CombinedTrendHintView(
|
||||
streakType: state.currentStreakInfo.type,
|
||||
streakCount: state.currentStreakInfo.count,
|
||||
hintInfo: state.currentHintInfo
|
||||
)
|
||||
.transition(.opacity)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.debugBorder(showDebugBorders, color: .purple, label: "TrendHint")
|
||||
}
|
||||
|
||||
Spacer(minLength: Design.Spacing.xSmall)
|
||||
@ -459,17 +457,15 @@ struct GameTableView: View, SherpaDelegate {
|
||||
.transition(.opacity)
|
||||
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
||||
|
||||
// Betting hint (static, below table, above chips)
|
||||
if let hintInfo = state.currentHintInfo {
|
||||
BettingHintView(
|
||||
hint: hintInfo.text,
|
||||
secondaryInfo: hintInfo.secondaryText,
|
||||
style: hintInfo.style
|
||||
)
|
||||
.transition(.opacity)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
||||
}
|
||||
// Combined trend and hint view (static, below table, above chips)
|
||||
CombinedTrendHintView(
|
||||
streakType: state.currentStreakInfo.type,
|
||||
streakCount: state.currentStreakInfo.count,
|
||||
hintInfo: state.currentHintInfo
|
||||
)
|
||||
.transition(.opacity)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.debugBorder(showDebugBorders, color: .purple, label: "TrendHint")
|
||||
}
|
||||
|
||||
// Chip selector - only shown during betting phase
|
||||
|
||||
@ -65,6 +65,19 @@ struct RulesHelpView: View {
|
||||
String(localized: "Banker bet has the lowest house edge (1.06%).")
|
||||
]
|
||||
),
|
||||
RulePage(
|
||||
title: String(localized: "Commission-Free Mode"),
|
||||
icon: "sparkles",
|
||||
content: [
|
||||
String(localized: "An alternative to standard Baccarat with simplified payouts."),
|
||||
String(localized: "Banker Bet: Pays 1:1 (no 5% commission)"),
|
||||
String(localized: "EXCEPT: If Banker wins with a total of 6, it's a push."),
|
||||
String(localized: "Your Banker bet is returned — you don't win or lose."),
|
||||
String(localized: "Player and Tie bets pay the same as standard Baccarat."),
|
||||
String(localized: "House edge is slightly higher (1.46%) but payouts are easier."),
|
||||
String(localized: "Enable in Settings → Game Variant.")
|
||||
]
|
||||
),
|
||||
RulePage(
|
||||
title: String(localized: "Third Card - Player"),
|
||||
icon: "hand.draw.fill",
|
||||
|
||||
@ -41,7 +41,15 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Deck Settings
|
||||
// 2. Game Variant
|
||||
SheetSection(title: String(localized: "GAME VARIANT"), icon: "sparkles") {
|
||||
GameVariantPicker(selection: $settings.gameVariant)
|
||||
.onChange(of: settings.gameVariant) { _, _ in
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Deck Settings
|
||||
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
|
||||
DeckCountPicker(selection: $settings.deckCount)
|
||||
.onChange(of: settings.deckCount) { _, _ in
|
||||
@ -476,6 +484,26 @@ struct RevealStylePicker: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Game Variant Picker (Baccarat-specific)
|
||||
|
||||
struct GameVariantPicker: View {
|
||||
@Binding var selection: BaccaratVariant
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
ForEach(BaccaratVariant.allCases) { variant in
|
||||
SelectableRow(
|
||||
title: variant.displayName,
|
||||
subtitle: variant.description,
|
||||
isSelected: selection == variant,
|
||||
accentColor: Color.Sheet.accent,
|
||||
action: { selection = variant }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView(settings: GameSettings(), gameState: GameState()) { }
|
||||
}
|
||||
|
||||
@ -68,6 +68,11 @@ struct BettingTableView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// Banker payout text depends on game variant
|
||||
private var bankerPayoutText: String {
|
||||
gameState.settings.gameVariant == .commissionFree ? "1 : 1" : "0.95 : 1"
|
||||
}
|
||||
|
||||
// Use global debug flag from Design constants
|
||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
||||
|
||||
@ -112,7 +117,7 @@ struct BettingTableView: View {
|
||||
// Middle row: BANKER | BONUS
|
||||
MainBetRow(
|
||||
title: "BANKER",
|
||||
payoutText: "0.95 : 1",
|
||||
payoutText: bankerPayoutText,
|
||||
mainBetAmount: betAmount(for: .banker),
|
||||
bonusBetAmount: betAmount(for: .dragonBonusBanker),
|
||||
isSelected: isBankerSelected,
|
||||
@ -122,6 +127,7 @@ struct BettingTableView: View {
|
||||
isBonusAtMax: isAtMax(for: .dragonBonusBanker),
|
||||
mainColor: Color.BettingZone.bankerDark,
|
||||
rowHeight: mainRowHeight,
|
||||
showBanker6Note: gameState.settings.gameVariant == .commissionFree,
|
||||
onMain: { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) },
|
||||
onBonus: { gameState.placeBet(type: .dragonBonusBanker, amount: selectedChip.rawValue) }
|
||||
)
|
||||
@ -145,6 +151,7 @@ struct BettingTableView: View {
|
||||
isBonusAtMax: isAtMax(for: .dragonBonusPlayer),
|
||||
mainColor: Color.BettingZone.playerDark,
|
||||
rowHeight: mainRowHeight,
|
||||
showBanker6Note: false,
|
||||
onMain: { gameState.placeBet(type: .player, amount: selectedChip.rawValue) },
|
||||
onBonus: { gameState.placeBet(type: .dragonBonusPlayer, amount: selectedChip.rawValue) }
|
||||
)
|
||||
@ -337,6 +344,7 @@ private struct MainBetRow: View {
|
||||
let isBonusAtMax: Bool
|
||||
let mainColor: Color
|
||||
let rowHeight: CGFloat
|
||||
let showBanker6Note: Bool
|
||||
let onMain: () -> Void
|
||||
let onBonus: () -> Void
|
||||
|
||||
@ -351,6 +359,7 @@ private struct MainBetRow: View {
|
||||
isEnabled: canBetMain,
|
||||
isAtMax: isMainAtMax,
|
||||
color: mainColor,
|
||||
showBanker6Note: showBanker6Note,
|
||||
action: onMain
|
||||
)
|
||||
|
||||
@ -382,8 +391,24 @@ private struct MainBetZone: View {
|
||||
let isEnabled: Bool
|
||||
let isAtMax: Bool
|
||||
let color: Color
|
||||
let showBanker6Note: Bool
|
||||
let action: () -> Void
|
||||
|
||||
/// Accessibility label with Banker 6 note if applicable
|
||||
private var accessibilityLabelText: String {
|
||||
var label = "\(title) bet, pays \(payoutText)"
|
||||
if showBanker6Note {
|
||||
label += ", Banker 6 pushes"
|
||||
}
|
||||
if isSelected {
|
||||
label += ", selected"
|
||||
}
|
||||
if betAmount > 0 {
|
||||
label += ", current bet $\(betAmount)"
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if isEnabled { action() }
|
||||
@ -407,12 +432,27 @@ private struct MainBetZone: View {
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .black, design: .rounded))
|
||||
.tracking(2)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text(payoutText)
|
||||
// Payout line - combined with 6 pushes note for Commission-Free
|
||||
if showBanker6Note {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Text(payoutText)
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||
|
||||
Text("•")
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
|
||||
Text("6 PUSHES")
|
||||
.foregroundStyle(.yellow.opacity(Design.Opacity.heavy))
|
||||
}
|
||||
.font(.system(size: Design.BaseFontSize.callout - 2, weight: .semibold, design: .rounded))
|
||||
.opacity(Design.Opacity.strong)
|
||||
} else {
|
||||
Text(payoutText)
|
||||
.font(.system(size: Design.BaseFontSize.callout - 2, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
|
||||
// Chip indicator - overlaid on right, doesn't affect centering
|
||||
if betAmount > 0 {
|
||||
@ -426,7 +466,7 @@ private struct MainBetZone: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("\(title) bet, pays \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
|
||||
.accessibilityLabel(accessibilityLabelText)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,8 +167,202 @@ struct StreakBadgeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Combined Trend and Hint View
|
||||
|
||||
/// A combined view showing streak information and betting hints side-by-side.
|
||||
/// Displays streak badge on the left (when present) and hint text on the right.
|
||||
struct CombinedTrendHintView: View {
|
||||
/// The current streak type (nil if no streak).
|
||||
let streakType: GameResult?
|
||||
|
||||
/// The streak count.
|
||||
let streakCount: Int
|
||||
|
||||
/// Minimum streak to display.
|
||||
var minimumStreak: Int = 2
|
||||
|
||||
/// The hint information (nil if no hint to show).
|
||||
let hintInfo: GameState.HintInfo?
|
||||
|
||||
// MARK: - Scaled Metrics
|
||||
|
||||
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = CasinoDesign.HintSize.iconSize
|
||||
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = CasinoDesign.HintSize.fontSize
|
||||
@ScaledMetric(relativeTo: .body) private var paddingH: CGFloat = CasinoDesign.HintSize.paddingH
|
||||
@ScaledMetric(relativeTo: .body) private var paddingV: CGFloat = CasinoDesign.HintSize.paddingV
|
||||
@ScaledMetric(relativeTo: .caption) private var streakFontSize: CGFloat = Design.BaseFontSize.small
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
private var shouldShowStreak: Bool {
|
||||
streakType != nil && streakType != .tie && streakCount >= minimumStreak
|
||||
}
|
||||
|
||||
private var hasContent: Bool {
|
||||
shouldShowStreak || hintInfo != nil
|
||||
}
|
||||
|
||||
private var streakColor: Color {
|
||||
guard let type = streakType else { return .clear }
|
||||
return type == .bankerWins ? .red : .blue
|
||||
}
|
||||
|
||||
private var streakText: String {
|
||||
guard let type = streakType else { return "" }
|
||||
let letter = type == .bankerWins ? "B" : "P"
|
||||
return "\(letter)×\(streakCount)"
|
||||
}
|
||||
|
||||
private var isHotStreak: Bool {
|
||||
streakCount >= 4
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
if hasContent {
|
||||
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||
// Streak badge (left side)
|
||||
if shouldShowStreak {
|
||||
streakBadge
|
||||
|
||||
// Separator when both are shown
|
||||
if hintInfo != nil {
|
||||
Text("•")
|
||||
.font(.system(size: fontSize, weight: .bold))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
}
|
||||
}
|
||||
|
||||
// Hint content (right side)
|
||||
if let hint = hintInfo {
|
||||
hintContent(hint: hint)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, paddingH)
|
||||
.padding(.vertical, paddingV)
|
||||
.frame(minWidth: CasinoDesign.HintSize.minWidth, alignment: .center)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(Color.black.opacity(CasinoDesign.Opacity.heavy))
|
||||
.overlay(
|
||||
Capsule()
|
||||
.strokeBorder(borderColor.opacity(CasinoDesign.Opacity.medium), lineWidth: CasinoDesign.LineWidth.thin)
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(CasinoDesign.Opacity.medium), radius: CasinoDesign.Shadow.radiusMedium)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private var streakBadge: some View {
|
||||
HStack(spacing: Design.Spacing.xxSmall) {
|
||||
Image(systemName: "flame.fill")
|
||||
.font(.system(size: streakFontSize))
|
||||
.foregroundStyle(isHotStreak ? .yellow : .orange)
|
||||
|
||||
Text(streakText)
|
||||
.font(.system(size: streakFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(streakColor)
|
||||
}
|
||||
.accessibilityLabel(streakAccessibilityLabel)
|
||||
}
|
||||
|
||||
private func hintContent(hint: GameState.HintInfo) -> some View {
|
||||
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||
Image(systemName: hint.style.icon)
|
||||
.font(.system(size: iconSize))
|
||||
.foregroundStyle(hint.style.color)
|
||||
|
||||
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.xxSmall) {
|
||||
Text(hint.text)
|
||||
.font(.system(size: fontSize, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(CasinoDesign.MinScaleFactor.comfortable)
|
||||
|
||||
if let secondary = hint.secondaryText {
|
||||
Text(secondary)
|
||||
.font(.system(size: fontSize - 2, weight: .regular))
|
||||
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(String(localized: "Betting Hint"))
|
||||
.accessibilityValue(hint.text)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var borderColor: Color {
|
||||
if shouldShowStreak {
|
||||
return streakColor
|
||||
} else if let hint = hintInfo {
|
||||
return hint.style.color
|
||||
}
|
||||
return .white
|
||||
}
|
||||
|
||||
private var streakAccessibilityLabel: String {
|
||||
guard let type = streakType else { return "" }
|
||||
let sideName = type == .bankerWins ?
|
||||
String(localized: "Banker") : String(localized: "Player")
|
||||
return String(localized: "\(sideName) streak of \(streakCount)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Combined - Both") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
CombinedTrendHintView(
|
||||
streakType: .bankerWins,
|
||||
streakCount: 5,
|
||||
hintInfo: GameState.HintInfo(
|
||||
text: "Banker running hot (65%)",
|
||||
secondaryText: nil,
|
||||
isStreak: false,
|
||||
isChoppy: false,
|
||||
isBankerHot: true,
|
||||
isPlayerHot: false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Combined - Streak Only") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
CombinedTrendHintView(
|
||||
streakType: .playerWins,
|
||||
streakCount: 4,
|
||||
hintInfo: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Combined - Hint Only") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
CombinedTrendHintView(
|
||||
streakType: nil,
|
||||
streakCount: 0,
|
||||
hintInfo: GameState.HintInfo(
|
||||
text: "Banker has the lowest house edge",
|
||||
secondaryText: nil,
|
||||
isStreak: false,
|
||||
isChoppy: false,
|
||||
isBankerHot: false,
|
||||
isPlayerHot: false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Distribution Bar") {
|
||||
ZStack {
|
||||
Color.Table.felt.ignoresSafeArea()
|
||||
|
||||
@ -22,7 +22,7 @@ public enum BettingHintStyle {
|
||||
/// Custom color
|
||||
case custom(Color, String)
|
||||
|
||||
var color: Color {
|
||||
public var color: Color {
|
||||
switch self {
|
||||
case .positive: return .green
|
||||
case .negative: return .red
|
||||
@ -33,7 +33,7 @@ public enum BettingHintStyle {
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
public var icon: String {
|
||||
switch self {
|
||||
case .positive: return "arrow.up.circle.fill"
|
||||
case .negative: return "arrow.down.circle.fill"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user