Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1e4e38b7ce
commit
77f5775cd2
@ -208,11 +208,12 @@ struct BaccaratEngine {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - bet: The bet that was placed.
|
/// - bet: The bet that was placed.
|
||||||
/// - result: The result of the round.
|
/// - 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.
|
/// - 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 {
|
switch bet.type {
|
||||||
case .player, .banker, .tie:
|
case .player, .banker, .tie:
|
||||||
return calculateMainBetPayout(bet: bet, result: result)
|
return calculateMainBetPayout(bet: bet, result: result, variant: variant)
|
||||||
|
|
||||||
case .playerPair:
|
case .playerPair:
|
||||||
return playerHasPair ? Int(Double(bet.amount) * bet.type.payoutMultiplier) : -bet.amount
|
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).
|
/// 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) {
|
if result.isPush(for: bet.type) {
|
||||||
// Push - bet is returned
|
|
||||||
return 0
|
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) {
|
if result.isWinningBet(bet.type) {
|
||||||
// Win - return winnings based on payout multiplier
|
// Win - return winnings based on variant-aware payout multiplier
|
||||||
return Int(Double(bet.amount) * bet.type.payoutMultiplier)
|
return Int(Double(bet.amount) * bet.type.payoutMultiplier(for: variant))
|
||||||
} else {
|
} else {
|
||||||
// Loss - lose the bet amount
|
// Loss - lose the bet amount
|
||||||
return -bet.amount
|
return -bet.amount
|
||||||
|
|||||||
@ -938,7 +938,7 @@ final class GameState: CasinoGameState {
|
|||||||
var dragonBankerWon = false
|
var dragonBankerWon = false
|
||||||
|
|
||||||
for bet in currentBets {
|
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
|
totalWinnings += payout
|
||||||
|
|
||||||
// Track individual bet result
|
// 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.
|
/// Display name with payout info.
|
||||||
var displayWithPayout: String {
|
var displayWithPayout: String {
|
||||||
switch self {
|
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.
|
/// The color associated with this bet type.
|
||||||
var color: Color {
|
var color: Color {
|
||||||
switch self {
|
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
|
// TableLimits is now provided by CasinoKit
|
||||||
|
|
||||||
/// Observable settings class for Baccarat configuration.
|
/// Observable settings class for Baccarat configuration.
|
||||||
@ -129,6 +151,11 @@ final class GameSettings: GameSettingsProtocol {
|
|||||||
/// Whether to show streak alerts.
|
/// Whether to show streak alerts.
|
||||||
var showStreakAlerts: Bool = true
|
var showStreakAlerts: Bool = true
|
||||||
|
|
||||||
|
// MARK: - Game Variant Settings
|
||||||
|
|
||||||
|
/// The baccarat game variant (standard vs commission-free).
|
||||||
|
var gameVariant: BaccaratVariant = .standard
|
||||||
|
|
||||||
// MARK: - Sound Settings
|
// MARK: - Sound Settings
|
||||||
|
|
||||||
/// Whether sound effects are enabled.
|
/// Whether sound effects are enabled.
|
||||||
@ -157,6 +184,7 @@ final class GameSettings: GameSettingsProtocol {
|
|||||||
static let revealStyle = "settings.revealStyle"
|
static let revealStyle = "settings.revealStyle"
|
||||||
static let preferredRoadType = "settings.preferredRoadType"
|
static let preferredRoadType = "settings.preferredRoadType"
|
||||||
static let showStreakAlerts = "settings.showStreakAlerts"
|
static let showStreakAlerts = "settings.showStreakAlerts"
|
||||||
|
static let gameVariant = "settings.gameVariant"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - iCloud
|
// MARK: - iCloud
|
||||||
@ -286,6 +314,11 @@ final class GameSettings: GameSettingsProtocol {
|
|||||||
if defaults.object(forKey: Keys.showStreakAlerts) != nil {
|
if defaults.object(forKey: Keys.showStreakAlerts) != nil {
|
||||||
self.showStreakAlerts = defaults.bool(forKey: Keys.showStreakAlerts)
|
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.
|
/// Loads settings from iCloud.
|
||||||
@ -351,6 +384,11 @@ final class GameSettings: GameSettingsProtocol {
|
|||||||
if store.object(forKey: Keys.showStreakAlerts) != nil {
|
if store.object(forKey: Keys.showStreakAlerts) != nil {
|
||||||
self.showStreakAlerts = store.bool(forKey: Keys.showStreakAlerts)
|
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.
|
/// Saves settings to UserDefaults and iCloud.
|
||||||
@ -371,6 +409,7 @@ final class GameSettings: GameSettingsProtocol {
|
|||||||
defaults.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
defaults.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
||||||
defaults.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
defaults.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
||||||
defaults.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
defaults.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
||||||
|
defaults.set(gameVariant.rawValue, forKey: Keys.gameVariant)
|
||||||
|
|
||||||
// Also save to iCloud
|
// Also save to iCloud
|
||||||
if iCloudAvailable, let store = iCloudStore {
|
if iCloudAvailable, let store = iCloudStore {
|
||||||
@ -388,6 +427,7 @@ final class GameSettings: GameSettingsProtocol {
|
|||||||
store.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
store.set(revealStyle.rawValue, forKey: Keys.revealStyle)
|
||||||
store.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
store.set(preferredRoadType.rawValue, forKey: Keys.preferredRoadType)
|
||||||
store.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
store.set(showStreakAlerts, forKey: Keys.showStreakAlerts)
|
||||||
|
store.set(gameVariant.rawValue, forKey: Keys.gameVariant)
|
||||||
store.synchronize()
|
store.synchronize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -426,6 +466,7 @@ final class GameSettings: GameSettingsProtocol {
|
|||||||
revealStyle = .auto
|
revealStyle = .auto
|
||||||
preferredRoadType = .big
|
preferredRoadType = .big
|
||||||
showStreakAlerts = true
|
showStreakAlerts = true
|
||||||
|
gameVariant = .standard
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -432,6 +432,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"6 PUSHES" : {
|
||||||
|
"comment" : "A note displayed for Banker 6 pushes in the baccarat betting table.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"8 : 1" : {
|
"8 : 1" : {
|
||||||
"comment" : "The payout ratio for a tie bet.",
|
"comment" : "The payout ratio for a tie bet.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -671,6 +675,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"An alternative to standard Baccarat with simplified payouts." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Analyzes patterns from the Big Road." : {
|
"Analyzes patterns from the Big Road." : {
|
||||||
"comment" : "Tooltip text for the \"Big Eye Boy\" road type in the road map interface.",
|
"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" : {
|
"Banker hand" : {
|
||||||
"comment" : "A label displayed above the banker's 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%%)" : {
|
"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.",
|
"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" : {
|
"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" : {
|
"Betting tips and trend analysis" : {
|
||||||
"comment" : "Description for hints feature.",
|
"comment" : "Description for hints feature.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1722,6 +1744,13 @@
|
|||||||
"Color Meaning" : {
|
"Color Meaning" : {
|
||||||
"comment" : "A heading displayed in the color legend of a derived road type popover.",
|
"comment" : "A heading displayed in the color legend of a derived road type popover.",
|
||||||
"isCommentAutoGenerated" : true
|
"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." : {
|
"Compares current streak to 2 columns back." : {
|
||||||
"comment" : "Tooltip text for the \"Small Road\" road type in the Road Map selector.",
|
"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" : {
|
"End Session" : {
|
||||||
"comment" : "The text for a button that ends the current game 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." : {
|
"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.",
|
"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" : {
|
"Generate & Save Icons" : {
|
||||||
"comment" : "A button label that triggers the generation of app 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" : {
|
"How to Export Icons" : {
|
||||||
"comment" : "A section header explaining how to export app 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)" : {
|
"Player Bet: Pays 1:1 (even money)" : {
|
||||||
"comment" : "Description of the payout for a Player Bet in the Rules Help view.",
|
"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.",
|
"comment" : "Name of the reveal style option that uses pressure-based card peeking.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Standard (5% Commission)" : {
|
||||||
|
"comment" : "Description of the \"Standard\" baccarat variant, including the commission rate.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"STARTING BALANCE" : {
|
"STARTING BALANCE" : {
|
||||||
"comment" : "Section header for starting balance settings.",
|
"comment" : "Section header for starting balance settings.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -5804,6 +5852,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Your Banker bet is returned — you don't win or lose." : {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.1"
|
"version" : "1.1"
|
||||||
|
|||||||
@ -324,14 +324,6 @@ struct GameTableView: View, SherpaDelegate {
|
|||||||
|
|
||||||
// Betting table - completely hidden during dealing
|
// Betting table - completely hidden during dealing
|
||||||
if !isDealing {
|
if !isDealing {
|
||||||
// Trend badge when there's an active streak
|
|
||||||
TrendBadgeView(
|
|
||||||
streakType: state.currentStreakInfo.type,
|
|
||||||
streakCount: state.currentStreakInfo.count,
|
|
||||||
minimumStreak: 2
|
|
||||||
)
|
|
||||||
.padding(.bottom, Design.Spacing.xSmall)
|
|
||||||
|
|
||||||
BettingTableView(
|
BettingTableView(
|
||||||
gameState: state,
|
gameState: state,
|
||||||
selectedChip: selectedChip
|
selectedChip: selectedChip
|
||||||
@ -341,17 +333,15 @@ struct GameTableView: View, SherpaDelegate {
|
|||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
||||||
|
|
||||||
// Betting hint (static, below table, above chips)
|
// Combined trend and hint view (static, below table, above chips)
|
||||||
if let hintInfo = state.currentHintInfo {
|
CombinedTrendHintView(
|
||||||
BettingHintView(
|
streakType: state.currentStreakInfo.type,
|
||||||
hint: hintInfo.text,
|
streakCount: state.currentStreakInfo.count,
|
||||||
secondaryInfo: hintInfo.secondaryText,
|
hintInfo: state.currentHintInfo
|
||||||
style: hintInfo.style
|
|
||||||
)
|
)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
.debugBorder(showDebugBorders, color: .purple, label: "TrendHint")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.xSmall)
|
Spacer(minLength: Design.Spacing.xSmall)
|
||||||
@ -458,14 +448,6 @@ struct GameTableView: View, SherpaDelegate {
|
|||||||
|
|
||||||
// Betting table - completely hidden during dealing
|
// Betting table - completely hidden during dealing
|
||||||
if !isDealing {
|
if !isDealing {
|
||||||
// Trend badge when there's an active streak
|
|
||||||
TrendBadgeView(
|
|
||||||
streakType: state.currentStreakInfo.type,
|
|
||||||
streakCount: state.currentStreakInfo.count,
|
|
||||||
minimumStreak: 2
|
|
||||||
)
|
|
||||||
.padding(.bottom, Design.Spacing.xSmall)
|
|
||||||
|
|
||||||
BettingTableView(
|
BettingTableView(
|
||||||
gameState: state,
|
gameState: state,
|
||||||
selectedChip: selectedChip
|
selectedChip: selectedChip
|
||||||
@ -475,17 +457,15 @@ struct GameTableView: View, SherpaDelegate {
|
|||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
||||||
|
|
||||||
// Betting hint (static, below table, above chips)
|
// Combined trend and hint view (static, below table, above chips)
|
||||||
if let hintInfo = state.currentHintInfo {
|
CombinedTrendHintView(
|
||||||
BettingHintView(
|
streakType: state.currentStreakInfo.type,
|
||||||
hint: hintInfo.text,
|
streakCount: state.currentStreakInfo.count,
|
||||||
secondaryInfo: hintInfo.secondaryText,
|
hintInfo: state.currentHintInfo
|
||||||
style: hintInfo.style
|
|
||||||
)
|
)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
.debugBorder(showDebugBorders, color: .purple, label: "TrendHint")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chip selector - only shown during betting phase
|
// 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%).")
|
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(
|
RulePage(
|
||||||
title: String(localized: "Third Card - Player"),
|
title: String(localized: "Third Card - Player"),
|
||||||
icon: "hand.draw.fill",
|
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") {
|
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
|
||||||
DeckCountPicker(selection: $settings.deckCount)
|
DeckCountPicker(selection: $settings.deckCount)
|
||||||
.onChange(of: settings.deckCount) { _, _ in
|
.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 {
|
#Preview {
|
||||||
SettingsView(settings: GameSettings(), gameState: GameState()) { }
|
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
|
// Use global debug flag from Design constants
|
||||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
private var showDebugBorders: Bool { Design.showDebugBorders }
|
||||||
|
|
||||||
@ -112,7 +117,7 @@ struct BettingTableView: View {
|
|||||||
// Middle row: BANKER | BONUS
|
// Middle row: BANKER | BONUS
|
||||||
MainBetRow(
|
MainBetRow(
|
||||||
title: "BANKER",
|
title: "BANKER",
|
||||||
payoutText: "0.95 : 1",
|
payoutText: bankerPayoutText,
|
||||||
mainBetAmount: betAmount(for: .banker),
|
mainBetAmount: betAmount(for: .banker),
|
||||||
bonusBetAmount: betAmount(for: .dragonBonusBanker),
|
bonusBetAmount: betAmount(for: .dragonBonusBanker),
|
||||||
isSelected: isBankerSelected,
|
isSelected: isBankerSelected,
|
||||||
@ -122,6 +127,7 @@ struct BettingTableView: View {
|
|||||||
isBonusAtMax: isAtMax(for: .dragonBonusBanker),
|
isBonusAtMax: isAtMax(for: .dragonBonusBanker),
|
||||||
mainColor: Color.BettingZone.bankerDark,
|
mainColor: Color.BettingZone.bankerDark,
|
||||||
rowHeight: mainRowHeight,
|
rowHeight: mainRowHeight,
|
||||||
|
showBanker6Note: gameState.settings.gameVariant == .commissionFree,
|
||||||
onMain: { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) },
|
onMain: { gameState.placeBet(type: .banker, amount: selectedChip.rawValue) },
|
||||||
onBonus: { gameState.placeBet(type: .dragonBonusBanker, amount: selectedChip.rawValue) }
|
onBonus: { gameState.placeBet(type: .dragonBonusBanker, amount: selectedChip.rawValue) }
|
||||||
)
|
)
|
||||||
@ -145,6 +151,7 @@ struct BettingTableView: View {
|
|||||||
isBonusAtMax: isAtMax(for: .dragonBonusPlayer),
|
isBonusAtMax: isAtMax(for: .dragonBonusPlayer),
|
||||||
mainColor: Color.BettingZone.playerDark,
|
mainColor: Color.BettingZone.playerDark,
|
||||||
rowHeight: mainRowHeight,
|
rowHeight: mainRowHeight,
|
||||||
|
showBanker6Note: false,
|
||||||
onMain: { gameState.placeBet(type: .player, amount: selectedChip.rawValue) },
|
onMain: { gameState.placeBet(type: .player, amount: selectedChip.rawValue) },
|
||||||
onBonus: { gameState.placeBet(type: .dragonBonusPlayer, amount: selectedChip.rawValue) }
|
onBonus: { gameState.placeBet(type: .dragonBonusPlayer, amount: selectedChip.rawValue) }
|
||||||
)
|
)
|
||||||
@ -337,6 +344,7 @@ private struct MainBetRow: View {
|
|||||||
let isBonusAtMax: Bool
|
let isBonusAtMax: Bool
|
||||||
let mainColor: Color
|
let mainColor: Color
|
||||||
let rowHeight: CGFloat
|
let rowHeight: CGFloat
|
||||||
|
let showBanker6Note: Bool
|
||||||
let onMain: () -> Void
|
let onMain: () -> Void
|
||||||
let onBonus: () -> Void
|
let onBonus: () -> Void
|
||||||
|
|
||||||
@ -351,6 +359,7 @@ private struct MainBetRow: View {
|
|||||||
isEnabled: canBetMain,
|
isEnabled: canBetMain,
|
||||||
isAtMax: isMainAtMax,
|
isAtMax: isMainAtMax,
|
||||||
color: mainColor,
|
color: mainColor,
|
||||||
|
showBanker6Note: showBanker6Note,
|
||||||
action: onMain
|
action: onMain
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -382,8 +391,24 @@ private struct MainBetZone: View {
|
|||||||
let isEnabled: Bool
|
let isEnabled: Bool
|
||||||
let isAtMax: Bool
|
let isAtMax: Bool
|
||||||
let color: Color
|
let color: Color
|
||||||
|
let showBanker6Note: Bool
|
||||||
let action: () -> Void
|
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 {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
if isEnabled { action() }
|
if isEnabled { action() }
|
||||||
@ -407,12 +432,27 @@ private struct MainBetZone: View {
|
|||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: Design.BaseFontSize.xLarge, weight: .black, design: .rounded))
|
.font(.system(size: Design.BaseFontSize.xLarge, weight: .black, design: .rounded))
|
||||||
.tracking(2)
|
.tracking(2)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
} else {
|
||||||
Text(payoutText)
|
Text(payoutText)
|
||||||
.font(.system(size: Design.BaseFontSize.callout - 2, weight: .semibold, design: .rounded))
|
.font(.system(size: Design.BaseFontSize.callout - 2, weight: .semibold, design: .rounded))
|
||||||
.opacity(Design.Opacity.strong)
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.foregroundStyle(.white)
|
|
||||||
|
|
||||||
// Chip indicator - overlaid on right, doesn't affect centering
|
// Chip indicator - overlaid on right, doesn't affect centering
|
||||||
if betAmount > 0 {
|
if betAmount > 0 {
|
||||||
@ -426,7 +466,7 @@ private struct MainBetZone: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel("\(title) bet, pays \(payoutText)" + (isSelected ? ", selected" : "") + (betAmount > 0 ? ", current bet $\(betAmount)" : ""))
|
.accessibilityLabel(accessibilityLabelText)
|
||||||
.accessibilityAddTraits(.isButton)
|
.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
|
// 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") {
|
#Preview("Distribution Bar") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.felt.ignoresSafeArea()
|
Color.Table.felt.ignoresSafeArea()
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public enum BettingHintStyle {
|
|||||||
/// Custom color
|
/// Custom color
|
||||||
case custom(Color, String)
|
case custom(Color, String)
|
||||||
|
|
||||||
var color: Color {
|
public var color: Color {
|
||||||
switch self {
|
switch self {
|
||||||
case .positive: return .green
|
case .positive: return .green
|
||||||
case .negative: return .red
|
case .negative: return .red
|
||||||
@ -33,7 +33,7 @@ public enum BettingHintStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var icon: String {
|
public var icon: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .positive: return "arrow.up.circle.fill"
|
case .positive: return "arrow.up.circle.fill"
|
||||||
case .negative: return "arrow.down.circle.fill"
|
case .negative: return "arrow.down.circle.fill"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user