Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
62c1cf4daf
commit
8082556d7e
@ -114,6 +114,206 @@ final class GameState {
|
|||||||
Array(roundHistory.suffix(20))
|
Array(roundHistory.suffix(20))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Hint System
|
||||||
|
|
||||||
|
/// The current streak type (player wins, banker wins, or alternating/none).
|
||||||
|
var currentStreakInfo: (type: GameResult?, count: Int) {
|
||||||
|
guard !roundHistory.isEmpty else { return (nil, 0) }
|
||||||
|
|
||||||
|
var streakType: GameResult?
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
// Count backwards from most recent, ignoring ties for streak purposes
|
||||||
|
for result in roundHistory.reversed() {
|
||||||
|
// Skip ties when counting streaks
|
||||||
|
if result.result == .tie {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if streakType == nil {
|
||||||
|
streakType = result.result
|
||||||
|
count = 1
|
||||||
|
} else if result.result == streakType {
|
||||||
|
count += 1
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (streakType, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distribution of results in the current session.
|
||||||
|
var resultDistribution: (player: Int, banker: Int, tie: Int) {
|
||||||
|
let player = roundHistory.filter { $0.result == .playerWins }.count
|
||||||
|
let banker = roundHistory.filter { $0.result == .bankerWins }.count
|
||||||
|
let tie = roundHistory.filter { $0.result == .tie }.count
|
||||||
|
return (player, banker, tie)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the game is "choppy" (alternating frequently between Player and Banker).
|
||||||
|
var isChoppy: Bool {
|
||||||
|
guard roundHistory.count >= 6 else { return false }
|
||||||
|
|
||||||
|
// Check last 6 non-tie results for alternating pattern
|
||||||
|
let recentNonTie = roundHistory.suffix(10).filter { $0.result != .tie }.suffix(6)
|
||||||
|
guard recentNonTie.count >= 6 else { return false }
|
||||||
|
|
||||||
|
var alternations = 0
|
||||||
|
var previous: GameResult?
|
||||||
|
|
||||||
|
for result in recentNonTie {
|
||||||
|
if let prev = previous, prev != result.result {
|
||||||
|
alternations += 1
|
||||||
|
}
|
||||||
|
previous = result.result
|
||||||
|
}
|
||||||
|
|
||||||
|
// If 4+ alternations in 6 hands, it's choppy
|
||||||
|
return alternations >= 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hint information including text and style for the shared BettingHintView.
|
||||||
|
struct HintInfo {
|
||||||
|
let text: String
|
||||||
|
let secondaryText: String?
|
||||||
|
let isStreak: Bool
|
||||||
|
let isChoppy: Bool
|
||||||
|
let isBankerHot: Bool
|
||||||
|
let isPlayerHot: Bool
|
||||||
|
|
||||||
|
/// Determines the style for CasinoKit.BettingHintView.
|
||||||
|
var style: BettingHintStyle {
|
||||||
|
if isStreak { return .streak }
|
||||||
|
if isChoppy { return .pattern }
|
||||||
|
if isBankerHot { return .custom(.red, "chart.line.uptrend.xyaxis") }
|
||||||
|
if isPlayerHot { return .custom(.blue, "chart.line.uptrend.xyaxis") }
|
||||||
|
return .neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current betting hint for beginners.
|
||||||
|
/// Returns nil if hints are disabled or no actionable hint is available.
|
||||||
|
var currentHint: String? {
|
||||||
|
currentHintInfo?.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full hint information including style.
|
||||||
|
var currentHintInfo: HintInfo? {
|
||||||
|
guard settings.showHints else { return nil }
|
||||||
|
guard currentPhase == .betting else { return nil }
|
||||||
|
|
||||||
|
// If no history, give the fundamental advice
|
||||||
|
if roundHistory.isEmpty {
|
||||||
|
return HintInfo(
|
||||||
|
text: String(localized: "Banker has the lowest house edge (1.06%)"),
|
||||||
|
secondaryText: nil,
|
||||||
|
isStreak: false,
|
||||||
|
isChoppy: false,
|
||||||
|
isBankerHot: false,
|
||||||
|
isPlayerHot: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let streak = currentStreakInfo
|
||||||
|
let dist = resultDistribution
|
||||||
|
let total = dist.player + dist.banker + dist.tie
|
||||||
|
|
||||||
|
// Calculate percentages if we have enough data
|
||||||
|
if total >= 5 {
|
||||||
|
let bankerPct = Double(dist.banker) / Double(total) * 100
|
||||||
|
let playerPct = Double(dist.player) / Double(total) * 100
|
||||||
|
let trendText = "P: \(Int(playerPct))% | B: \(Int(bankerPct))%"
|
||||||
|
|
||||||
|
// Strong streak (4+): suggest following or note it
|
||||||
|
if let streakType = streak.type, streak.count >= 4 {
|
||||||
|
let streakName = streakType == .bankerWins ?
|
||||||
|
String(localized: "Banker") : String(localized: "Player")
|
||||||
|
return HintInfo(
|
||||||
|
text: String(localized: "\(streakName) streak: \(streak.count) in a row"),
|
||||||
|
secondaryText: trendText,
|
||||||
|
isStreak: true,
|
||||||
|
isChoppy: false,
|
||||||
|
isBankerHot: false,
|
||||||
|
isPlayerHot: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choppy game: note the pattern
|
||||||
|
if isChoppy {
|
||||||
|
return HintInfo(
|
||||||
|
text: String(localized: "Choppy shoe - results alternating"),
|
||||||
|
secondaryText: trendText,
|
||||||
|
isStreak: false,
|
||||||
|
isChoppy: true,
|
||||||
|
isBankerHot: false,
|
||||||
|
isPlayerHot: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Significant imbalance (15%+ difference)
|
||||||
|
if bankerPct > playerPct + 15 {
|
||||||
|
return HintInfo(
|
||||||
|
text: String(localized: "Banker running hot (\(Int(bankerPct))%)"),
|
||||||
|
secondaryText: nil,
|
||||||
|
isStreak: false,
|
||||||
|
isChoppy: false,
|
||||||
|
isBankerHot: true,
|
||||||
|
isPlayerHot: false
|
||||||
|
)
|
||||||
|
} else if playerPct > bankerPct + 15 {
|
||||||
|
return HintInfo(
|
||||||
|
text: String(localized: "Player running hot (\(Int(playerPct))%)"),
|
||||||
|
secondaryText: nil,
|
||||||
|
isStreak: false,
|
||||||
|
isChoppy: false,
|
||||||
|
isBankerHot: false,
|
||||||
|
isPlayerHot: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default hint: remind about odds
|
||||||
|
return HintInfo(
|
||||||
|
text: String(localized: "Banker bet has lowest house edge"),
|
||||||
|
secondaryText: nil,
|
||||||
|
isStreak: false,
|
||||||
|
isChoppy: false,
|
||||||
|
isBankerHot: false,
|
||||||
|
isPlayerHot: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short trend summary for the top bar or compact display.
|
||||||
|
var trendSummary: String? {
|
||||||
|
guard settings.showHints else { return nil }
|
||||||
|
guard !roundHistory.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let streak = currentStreakInfo
|
||||||
|
if let streakType = streak.type, streak.count >= 2 {
|
||||||
|
let letter = streakType == .bankerWins ? "B" : "P"
|
||||||
|
return "\(letter)×\(streak.count)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Warning message for high house edge bets.
|
||||||
|
func warningForBet(_ type: BetType) -> String? {
|
||||||
|
guard settings.showHints else { return nil }
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .tie:
|
||||||
|
return String(localized: "Tie has 14% house edge")
|
||||||
|
case .playerPair, .bankerPair:
|
||||||
|
return String(localized: "Pair bets have ~10% house edge")
|
||||||
|
case .dragonBonusPlayer, .dragonBonusBanker:
|
||||||
|
return String(localized: "Dragon Bonus: high risk, high reward")
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Animation Timing (based on settings)
|
// MARK: - Animation Timing (based on settings)
|
||||||
|
|
||||||
private var dealDelay: Duration {
|
private var dealDelay: Duration {
|
||||||
|
|||||||
@ -133,6 +133,9 @@ final class GameSettings {
|
|||||||
/// Whether to show the history road map.
|
/// Whether to show the history road map.
|
||||||
var showHistory: Bool = true
|
var showHistory: Bool = true
|
||||||
|
|
||||||
|
/// Whether to show betting hints and recommendations.
|
||||||
|
var showHints: Bool = true
|
||||||
|
|
||||||
// MARK: - Sound Settings
|
// MARK: - Sound Settings
|
||||||
|
|
||||||
/// Whether sound effects are enabled.
|
/// Whether sound effects are enabled.
|
||||||
@ -154,6 +157,7 @@ final class GameSettings {
|
|||||||
static let dealingSpeed = "settings.dealingSpeed"
|
static let dealingSpeed = "settings.dealingSpeed"
|
||||||
static let showCardsRemaining = "settings.showCardsRemaining"
|
static let showCardsRemaining = "settings.showCardsRemaining"
|
||||||
static let showHistory = "settings.showHistory"
|
static let showHistory = "settings.showHistory"
|
||||||
|
static let showHints = "settings.showHints"
|
||||||
static let soundEnabled = "settings.soundEnabled"
|
static let soundEnabled = "settings.soundEnabled"
|
||||||
static let hapticsEnabled = "settings.hapticsEnabled"
|
static let hapticsEnabled = "settings.hapticsEnabled"
|
||||||
static let soundVolume = "settings.soundVolume"
|
static let soundVolume = "settings.soundVolume"
|
||||||
@ -237,6 +241,10 @@ final class GameSettings {
|
|||||||
self.showHistory = defaults.bool(forKey: Keys.showHistory)
|
self.showHistory = defaults.bool(forKey: Keys.showHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if defaults.object(forKey: Keys.showHints) != nil {
|
||||||
|
self.showHints = defaults.bool(forKey: Keys.showHints)
|
||||||
|
}
|
||||||
|
|
||||||
if defaults.object(forKey: Keys.soundEnabled) != nil {
|
if defaults.object(forKey: Keys.soundEnabled) != nil {
|
||||||
self.soundEnabled = defaults.bool(forKey: Keys.soundEnabled)
|
self.soundEnabled = defaults.bool(forKey: Keys.soundEnabled)
|
||||||
}
|
}
|
||||||
@ -284,6 +292,10 @@ final class GameSettings {
|
|||||||
self.showHistory = iCloudStore.bool(forKey: Keys.showHistory)
|
self.showHistory = iCloudStore.bool(forKey: Keys.showHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if iCloudStore.object(forKey: Keys.showHints) != nil {
|
||||||
|
self.showHints = iCloudStore.bool(forKey: Keys.showHints)
|
||||||
|
}
|
||||||
|
|
||||||
if iCloudStore.object(forKey: Keys.soundEnabled) != nil {
|
if iCloudStore.object(forKey: Keys.soundEnabled) != nil {
|
||||||
self.soundEnabled = iCloudStore.bool(forKey: Keys.soundEnabled)
|
self.soundEnabled = iCloudStore.bool(forKey: Keys.soundEnabled)
|
||||||
}
|
}
|
||||||
@ -308,6 +320,7 @@ final class GameSettings {
|
|||||||
defaults.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
defaults.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
||||||
defaults.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
defaults.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
||||||
defaults.set(showHistory, forKey: Keys.showHistory)
|
defaults.set(showHistory, forKey: Keys.showHistory)
|
||||||
|
defaults.set(showHints, forKey: Keys.showHints)
|
||||||
defaults.set(soundEnabled, forKey: Keys.soundEnabled)
|
defaults.set(soundEnabled, forKey: Keys.soundEnabled)
|
||||||
defaults.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
defaults.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
||||||
defaults.set(soundVolume, forKey: Keys.soundVolume)
|
defaults.set(soundVolume, forKey: Keys.soundVolume)
|
||||||
@ -321,6 +334,7 @@ final class GameSettings {
|
|||||||
iCloudStore.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
iCloudStore.set(dealingSpeed, forKey: Keys.dealingSpeed)
|
||||||
iCloudStore.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
iCloudStore.set(showCardsRemaining, forKey: Keys.showCardsRemaining)
|
||||||
iCloudStore.set(showHistory, forKey: Keys.showHistory)
|
iCloudStore.set(showHistory, forKey: Keys.showHistory)
|
||||||
|
iCloudStore.set(showHints, forKey: Keys.showHints)
|
||||||
iCloudStore.set(soundEnabled, forKey: Keys.soundEnabled)
|
iCloudStore.set(soundEnabled, forKey: Keys.soundEnabled)
|
||||||
iCloudStore.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
iCloudStore.set(hapticsEnabled, forKey: Keys.hapticsEnabled)
|
||||||
iCloudStore.set(Double(soundVolume), forKey: Keys.soundVolume)
|
iCloudStore.set(Double(soundVolume), forKey: Keys.soundVolume)
|
||||||
@ -337,6 +351,7 @@ final class GameSettings {
|
|||||||
dealingSpeed = 1.0
|
dealingSpeed = 1.0
|
||||||
showCardsRemaining = true
|
showCardsRemaining = true
|
||||||
showHistory = true
|
showHistory = true
|
||||||
|
showHints = true
|
||||||
soundEnabled = true
|
soundEnabled = true
|
||||||
hapticsEnabled = true
|
hapticsEnabled = true
|
||||||
soundVolume = 1.0
|
soundVolume = 1.0
|
||||||
|
|||||||
@ -23,6 +23,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"%@ streak of %lld" : {
|
||||||
|
"comment" : "A label describing the type of streak and its count. The first argument is the type of streak (\"Banker\" or \"Player\"). The second argument is the count of the streak.",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ streak of %2$lld"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"%@ streak: %lld in a row" : {
|
||||||
|
"comment" : "Text displayed as a hint in the game UI, providing information about a streak of wins for either the Player or Banker. The argument is the name of the player/banker involved in the streak, and the second argument is the count of consecutive wins.",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ streak: %2$lld in a row"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"%@×%lld" : {
|
||||||
|
"comment" : "A text view displaying the streak count and type (e.g., \"3×Banker\"). The first argument is the streak count. The second argument is the type of streak (\"Banker\" or \"Player\").",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@×%2$lld"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"%lld" : {
|
"%lld" : {
|
||||||
"comment" : "The number of rounds a player has played in the game.",
|
"comment" : "The number of rounds a player has played in the game.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -685,6 +721,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"B: %lld%%" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Baccarat has one of the lowest house edges in the casino." : {
|
"Baccarat has one of the lowest house edges in the casino." : {
|
||||||
"comment" : "Description of the house edge of baccarat.",
|
"comment" : "Description of the house edge of baccarat.",
|
||||||
@ -892,6 +931,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Banker bet has lowest house edge" : {
|
||||||
|
"comment" : "Default hint in the game, reminding players about the house edge of the banker bet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Banker bet has the lowest house edge (1.06%)." : {
|
"Banker bet has the lowest house edge (1.06%)." : {
|
||||||
"comment" : "Description of the house edge for the Banker bet in the Rules Help view.",
|
"comment" : "Description of the house edge for the Banker bet in the Rules Help view.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -961,6 +1004,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Banker has the lowest house edge (1.06%)" : {
|
||||||
|
"comment" : "Hint text for beginners about the house edge of the banker bet in a baccarat round.",
|
||||||
|
"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.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Bet on which hand will win: Player, Banker, or Tie." : {
|
"Bet on which hand will win: Player, Banker, or Tie." : {
|
||||||
"comment" : "Text describing the objective of the baccarat game.",
|
"comment" : "Text describing the objective of the baccarat game.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -983,6 +1034,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Betting tips and trend analysis" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Blackjack" : {
|
"Blackjack" : {
|
||||||
"comment" : "The name of a blackjack game.",
|
"comment" : "The name of a blackjack game.",
|
||||||
@ -1168,6 +1222,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Choppy shoe - results alternating" : {
|
||||||
|
"comment" : "Hint text indicating that the current shoe is \"choppy\", with results alternating between Player and Banker.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Clear" : {
|
"Clear" : {
|
||||||
"comment" : "The label of a button that clears all current bets in the game.",
|
"comment" : "The label of a button that clears all current bets in the game.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1305,6 +1363,7 @@
|
|||||||
},
|
},
|
||||||
"Dealing..." : {
|
"Dealing..." : {
|
||||||
"comment" : "A placeholder text shown while a game is being dealt.",
|
"comment" : "A placeholder text shown while a game is being dealt.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1486,6 +1545,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Dragon Bonus: high risk, high reward" : {
|
||||||
|
"comment" : "Warning text for dragon bonus bets, advising players to be cautious.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Example: 5♥ + 5♣ = Pair (wins!)" : {
|
"Example: 5♥ + 5♣ = Pair (wins!)" : {
|
||||||
"comment" : "Example of a pair bet winning.",
|
"comment" : "Example of a pair bet winning.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2514,6 +2577,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"P: %lld%%" : {
|
||||||
|
"comment" : "Labels indicating the percentage of Player, Banker, and Tie outcomes in a game's result distribution.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Pair bets have ~10% house edge" : {
|
||||||
|
"comment" : "Warning message for high house edge pair bets.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Pair bets have ~10% house edge." : {
|
"Pair bets have ~10% house edge." : {
|
||||||
"comment" : "Description of the house edge of a pair bet in the Rules Help view.",
|
"comment" : "Description of the house edge of a pair bet in the Rules Help view.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2675,6 +2746,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Player %lld%%, Banker %lld%%, Tie %lld%%" : {
|
||||||
|
"comment" : "A label describing the percentage of times the player, banker, or tie result occurred in the last spin of the game.",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "Player %1$lld%%, Banker %2$lld%%, Tie %3$lld%%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"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.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2721,6 +2804,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Player running hot (%lld%%)" : {
|
||||||
|
"comment" : "A hint to place a bet on the Player, given a significant house edge in favor of the Player. The percentage is included as a context hint.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Player with 0-5: Draws a third card" : {
|
"Player with 0-5: Draws a third card" : {
|
||||||
"comment" : "Description of the action for the Player when their third card is 0-5.",
|
"comment" : "Description of the action for the Player when their third card is 0-5.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2904,6 +2991,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Result distribution" : {
|
||||||
|
"comment" : "A label describing the view that shows the distribution of betting results.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Roulette" : {
|
"Roulette" : {
|
||||||
"comment" : "The name of a roulette game.",
|
"comment" : "The name of a roulette game.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -3041,6 +3132,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Show Hints" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Show History" : {
|
"Show History" : {
|
||||||
"comment" : "Toggle label for showing game history.",
|
"comment" : "Toggle label for showing game history.",
|
||||||
@ -3247,6 +3341,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Streak" : {
|
||||||
|
"comment" : "An accessibility label for the streak badge.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Sync Now" : {
|
"Sync Now" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -3291,6 +3389,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"T: %lld%%" : {
|
||||||
|
"comment" : "A label indicating that there are ties in the distribution.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"TABLE LIMITS" : {
|
"TABLE LIMITS" : {
|
||||||
"comment" : "Section header for table limits settings.",
|
"comment" : "Section header for table limits settings.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -3543,6 +3645,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Tie has 14% house edge" : {
|
||||||
|
"comment" : "Warning message for a tie bet, explaining the high house edge.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"TOTAL" : {
|
"TOTAL" : {
|
||||||
"comment" : "A label displayed next to the total winnings in the result banner.",
|
"comment" : "A label displayed next to the total winnings in the result banner.",
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import CasinoKit
|
|||||||
enum Design {
|
enum Design {
|
||||||
|
|
||||||
/// Set to true to show layout debug borders on views
|
/// Set to true to show layout debug borders on views
|
||||||
static let showDebugBorders = false
|
static let showDebugBorders = true
|
||||||
|
|
||||||
// MARK: - Shared Constants (from CasinoKit)
|
// MARK: - Shared Constants (from CasinoKit)
|
||||||
|
|
||||||
@ -28,6 +28,8 @@ enum Design {
|
|||||||
typealias MinScaleFactor = CasinoDesign.MinScaleFactor
|
typealias MinScaleFactor = CasinoDesign.MinScaleFactor
|
||||||
typealias BaseFontSize = CasinoDesign.BaseFontSize
|
typealias BaseFontSize = CasinoDesign.BaseFontSize
|
||||||
typealias IconSize = CasinoDesign.IconSize
|
typealias IconSize = CasinoDesign.IconSize
|
||||||
|
typealias Toast = CasinoDesign.Toast
|
||||||
|
typealias HintSize = CasinoDesign.HintSize
|
||||||
|
|
||||||
// MARK: - Baccarat-Specific Sizes (use CasinoDesign.Size for shared values)
|
// MARK: - Baccarat-Specific Sizes (use CasinoDesign.Size for shared values)
|
||||||
|
|
||||||
|
|||||||
@ -212,6 +212,18 @@ struct GameTableView: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
.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")
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.xSmall)
|
Spacer(minLength: Design.Spacing.xSmall)
|
||||||
|
|
||||||
// Chip selector - full width so all chips are tappable
|
// Chip selector - full width so all chips are tappable
|
||||||
@ -306,6 +318,18 @@ struct GameTableView: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.debugBorder(showDebugBorders, color: .blue, label: "BetTable")
|
.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")
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(minLength: mediumSpacerHeight)
|
Spacer(minLength: mediumSpacerHeight)
|
||||||
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer4")
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer4")
|
||||||
|
|
||||||
|
|||||||
@ -92,6 +92,16 @@ struct SettingsView: View {
|
|||||||
isOn: $settings.showHistory,
|
isOn: $settings.showHistory,
|
||||||
accentColor: accent
|
accentColor: accent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
||||||
|
|
||||||
|
SettingsToggle(
|
||||||
|
title: String(localized: "Show Hints"),
|
||||||
|
subtitle: String(localized: "Betting tips and trend analysis"),
|
||||||
|
isOn: $settings.showHints,
|
||||||
|
accentColor: accent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Sound & Haptics
|
// 5. Sound & Haptics
|
||||||
|
|||||||
195
Baccarat/Baccarat/Views/Table/HintViews.swift
Normal file
195
Baccarat/Baccarat/Views/Table/HintViews.swift
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
//
|
||||||
|
// HintViews.swift
|
||||||
|
// Baccarat
|
||||||
|
//
|
||||||
|
// Baccarat-specific hint views and supplementary components.
|
||||||
|
// The main BettingHintView is provided by CasinoKit.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import CasinoKit
|
||||||
|
|
||||||
|
// MARK: - Odds Info View
|
||||||
|
|
||||||
|
/// Shows the house edge information for educational purposes.
|
||||||
|
struct OddsInfoView: View {
|
||||||
|
@ScaledMetric(relativeTo: .caption) private var fontSize: CGFloat = Design.BaseFontSize.xSmall
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
oddsItem(label: "Banker", edge: "1.06%", color: .red)
|
||||||
|
oddsItem(label: "Player", edge: "1.24%", color: .blue)
|
||||||
|
oddsItem(label: "Tie", edge: "14.4%", color: .green)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
||||||
|
.fill(Color.black.opacity(Design.Opacity.light))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func oddsItem(label: String, edge: String, color: Color) -> some View {
|
||||||
|
VStack(spacing: Design.Spacing.xxSmall) {
|
||||||
|
Text(label)
|
||||||
|
.font(.system(size: fontSize, weight: .bold))
|
||||||
|
.foregroundStyle(color)
|
||||||
|
Text(edge)
|
||||||
|
.font(.system(size: fontSize, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Distribution Bar
|
||||||
|
|
||||||
|
/// A visual bar showing the distribution of Player/Banker/Tie results.
|
||||||
|
struct DistributionBarView: View {
|
||||||
|
let player: Int
|
||||||
|
let banker: Int
|
||||||
|
let tie: Int
|
||||||
|
|
||||||
|
@ScaledMetric(relativeTo: .caption) private var fontSize: CGFloat = Design.BaseFontSize.xSmall
|
||||||
|
|
||||||
|
private var total: Int { player + banker + tie }
|
||||||
|
|
||||||
|
private var playerPct: Double {
|
||||||
|
total > 0 ? Double(player) / Double(total) : 0.33
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bankerPct: Double {
|
||||||
|
total > 0 ? Double(banker) / Double(total) : 0.33
|
||||||
|
}
|
||||||
|
|
||||||
|
private var tiePct: Double {
|
||||||
|
total > 0 ? Double(tie) / Double(total) : 0.34
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: Design.Spacing.xSmall) {
|
||||||
|
// Bar
|
||||||
|
GeometryReader { geo in
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.blue)
|
||||||
|
.frame(width: geo.size.width * playerPct)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.green)
|
||||||
|
.frame(width: geo.size.width * tiePct)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.red)
|
||||||
|
.frame(width: geo.size.width * bankerPct)
|
||||||
|
}
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.frame(height: Design.Spacing.small)
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
HStack {
|
||||||
|
Text("P: \(Int(playerPct * 100))%")
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if tie > 0 {
|
||||||
|
Text("T: \(Int(tiePct * 100))%")
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("B: \(Int(bankerPct * 100))%")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
.font(.system(size: fontSize, weight: .medium, design: .rounded))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
||||||
|
.fill(Color.black.opacity(Design.Opacity.light))
|
||||||
|
)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(String(localized: "Result distribution"))
|
||||||
|
.accessibilityValue("Player \(Int(playerPct * 100))%, Banker \(Int(bankerPct * 100))%, Tie \(Int(tiePct * 100))%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Streak Badge
|
||||||
|
|
||||||
|
/// A badge showing the current streak.
|
||||||
|
struct StreakBadgeView: View {
|
||||||
|
let type: GameResult
|
||||||
|
let count: Int
|
||||||
|
|
||||||
|
@ScaledMetric(relativeTo: .caption) private var fontSize: CGFloat = Design.BaseFontSize.body
|
||||||
|
|
||||||
|
private var color: Color {
|
||||||
|
switch type {
|
||||||
|
case .bankerWins: return .red
|
||||||
|
case .playerWins: return .blue
|
||||||
|
case .tie: return .green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var letter: String {
|
||||||
|
switch type {
|
||||||
|
case .bankerWins: return "B"
|
||||||
|
case .playerWins: return "P"
|
||||||
|
case .tie: return "T"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: Design.Spacing.xxSmall) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.font(.system(size: fontSize))
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
|
||||||
|
Text("\(letter)×\(count)")
|
||||||
|
.font(.system(size: fontSize, weight: .bold, design: .rounded))
|
||||||
|
.foregroundStyle(color)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.small)
|
||||||
|
.padding(.vertical, Design.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.black.opacity(Design.Opacity.heavy))
|
||||||
|
.overlay(
|
||||||
|
Capsule()
|
||||||
|
.strokeBorder(color.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.accessibilityLabel(String(localized: "Streak"))
|
||||||
|
.accessibilityValue("\(type == .bankerWins ? "Banker" : "Player") streak of \(count)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
#Preview("Distribution Bar") {
|
||||||
|
ZStack {
|
||||||
|
Color.Table.felt.ignoresSafeArea()
|
||||||
|
DistributionBarView(player: 12, banker: 15, tie: 3)
|
||||||
|
.frame(maxWidth: 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Streak Badge") {
|
||||||
|
ZStack {
|
||||||
|
Color.Table.felt.ignoresSafeArea()
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
StreakBadgeView(type: .bankerWins, count: 4)
|
||||||
|
StreakBadgeView(type: .playerWins, count: 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Odds Info") {
|
||||||
|
ZStack {
|
||||||
|
Color.Table.felt.ignoresSafeArea()
|
||||||
|
OddsInfoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1456,6 +1456,7 @@
|
|||||||
},
|
},
|
||||||
"Betting Hint" : {
|
"Betting Hint" : {
|
||||||
"comment" : "A label describing the view that shows betting recommendations.",
|
"comment" : "A label describing the view that shows betting recommendations.",
|
||||||
|
"extractionState" : "stale",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ enum Design {
|
|||||||
// MARK: - Debug
|
// MARK: - Debug
|
||||||
|
|
||||||
/// Set to true to show layout debug borders on views
|
/// Set to true to show layout debug borders on views
|
||||||
static let showDebugBorders = false
|
static let showDebugBorders = true
|
||||||
|
|
||||||
/// Set to true to show debug log statements
|
/// Set to true to show debug log statements
|
||||||
static let showDebugLogs = false
|
static let showDebugLogs = false
|
||||||
|
|||||||
@ -215,7 +215,7 @@ struct BlackjackTableView: View {
|
|||||||
|
|
||||||
// Betting hint based on count (only when card counting enabled)
|
// Betting hint based on count (only when card counting enabled)
|
||||||
if let hint = state.bettingHint {
|
if let hint = state.bettingHint {
|
||||||
BettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
BlackjackBettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
|
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
|
||||||
|
|||||||
@ -48,64 +48,28 @@ struct HintView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Betting Hint View
|
// MARK: - Blackjack Betting Hint View
|
||||||
|
|
||||||
/// Shows betting recommendations based on the current card count.
|
/// Shows betting recommendations based on the current card count.
|
||||||
struct BettingHintView: View {
|
/// Maps true count to hint style and uses CasinoKit's shared BettingHintView.
|
||||||
|
struct BlackjackBettingHintView: View {
|
||||||
let hint: String
|
let hint: String
|
||||||
let trueCount: Double
|
let trueCount: Double
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.hintIconSize
|
/// Determines the hint style based on true count.
|
||||||
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.Size.hintFontSize
|
private var style: BettingHintStyle {
|
||||||
@ScaledMetric(relativeTo: .body) private var paddingH: CGFloat = Design.Size.hintPaddingH
|
|
||||||
@ScaledMetric(relativeTo: .body) private var paddingV: CGFloat = Design.Size.hintPaddingV
|
|
||||||
|
|
||||||
private var hintColor: Color {
|
|
||||||
let tc = Int(trueCount.rounded())
|
let tc = Int(trueCount.rounded())
|
||||||
if tc >= 2 {
|
if tc >= 2 {
|
||||||
return .green // Player advantage - bet more
|
return .positive // Player advantage - bet more
|
||||||
} else if tc <= -1 {
|
} else if tc <= -1 {
|
||||||
return .red // House advantage - bet less
|
return .negative // House advantage - bet less
|
||||||
} else {
|
} else {
|
||||||
return .yellow // Neutral
|
return .neutral // Neutral
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var icon: String {
|
|
||||||
let tc = Int(trueCount.rounded())
|
|
||||||
if tc >= 2 {
|
|
||||||
return "arrow.up.circle.fill" // Increase bet
|
|
||||||
} else if tc <= -1 {
|
|
||||||
return "arrow.down.circle.fill" // Decrease bet
|
|
||||||
} else {
|
|
||||||
return "equal.circle.fill" // Neutral
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: Design.Spacing.small) {
|
BettingHintView(hint: hint, style: style)
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.system(size: iconSize))
|
|
||||||
.foregroundStyle(hintColor)
|
|
||||||
Text(hint)
|
|
||||||
.font(.system(size: fontSize, weight: .medium))
|
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
||||||
.lineLimit(1)
|
|
||||||
.minimumScaleFactor(Design.MinScaleFactor.comfortable)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, paddingH)
|
|
||||||
.padding(.vertical, paddingV)
|
|
||||||
.background(
|
|
||||||
Capsule()
|
|
||||||
.fill(Color.black.opacity(Design.Opacity.light))
|
|
||||||
.overlay(
|
|
||||||
Capsule()
|
|
||||||
.strokeBorder(hintColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.accessibilityElement(children: .ignore)
|
|
||||||
.accessibilityLabel(String(localized: "Betting Hint"))
|
|
||||||
.accessibilityValue(hint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,21 +92,21 @@ struct BettingHintView: View {
|
|||||||
#Preview("Betting Hint - Positive Count") {
|
#Preview("Betting Hint - Positive Count") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.felt.ignoresSafeArea()
|
Color.Table.felt.ignoresSafeArea()
|
||||||
BettingHintView(hint: "Bet 4x minimum", trueCount: 2.5)
|
BlackjackBettingHintView(hint: "Bet 4x minimum", trueCount: 2.5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Betting Hint - Negative Count") {
|
#Preview("Betting Hint - Negative Count") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.felt.ignoresSafeArea()
|
Color.Table.felt.ignoresSafeArea()
|
||||||
BettingHintView(hint: "Bet minimum", trueCount: -1.5)
|
BlackjackBettingHintView(hint: "Bet minimum", trueCount: -1.5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Betting Hint - Neutral") {
|
#Preview("Betting Hint - Neutral") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.felt.ignoresSafeArea()
|
Color.Table.felt.ignoresSafeArea()
|
||||||
BettingHintView(hint: "Bet minimum (neutral)", trueCount: 0.0)
|
BlackjackBettingHintView(hint: "Bet minimum (neutral)", trueCount: 0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,10 @@
|
|||||||
// - ValueBadge
|
// - ValueBadge
|
||||||
// - ChipBadge
|
// - ChipBadge
|
||||||
|
|
||||||
|
// MARK: - Hints
|
||||||
|
// - BettingHintView (shared betting recommendation view)
|
||||||
|
// - BettingHintStyle (positive, negative, neutral, streak, pattern, custom)
|
||||||
|
|
||||||
// MARK: - Buttons
|
// MARK: - Buttons
|
||||||
// - ActionButton, ActionButtonStyle
|
// - ActionButton, ActionButtonStyle
|
||||||
// - BettingActionsView (Clear/Deal button pair for betting phase)
|
// - BettingActionsView (Clear/Deal button pair for betting phase)
|
||||||
|
|||||||
@ -474,6 +474,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Betting Hint" : {
|
||||||
|
"comment" : "The accessibility label for the betting hint view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Card face down" : {
|
"Card face down" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -229,6 +229,23 @@ public enum CasinoDesign {
|
|||||||
public enum ChipStack {
|
public enum ChipStack {
|
||||||
public static let maxChipsToShow: Int = 5
|
public static let maxChipsToShow: Int = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Toast Configuration
|
||||||
|
|
||||||
|
public enum Toast {
|
||||||
|
/// Duration toasts stay visible before auto-dismiss.
|
||||||
|
public static let duration: Duration = .seconds(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Hint Sizes
|
||||||
|
|
||||||
|
public enum HintSize {
|
||||||
|
public static let iconSize: CGFloat = 16
|
||||||
|
public static let fontSize: CGFloat = 13
|
||||||
|
public static let paddingH: CGFloat = 12
|
||||||
|
public static let paddingV: CGFloat = 8
|
||||||
|
public static let minWidth: CGFloat = 180
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - CasinoKit Colors
|
// MARK: - CasinoKit Colors
|
||||||
|
|||||||
171
CasinoKit/Sources/CasinoKit/Views/BettingHintView.swift
Normal file
171
CasinoKit/Sources/CasinoKit/Views/BettingHintView.swift
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
//
|
||||||
|
// BettingHintView.swift
|
||||||
|
// CasinoKit
|
||||||
|
//
|
||||||
|
// A shared betting hint view for displaying recommendations across casino games.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Style of the betting hint (affects color and icon).
|
||||||
|
public enum BettingHintStyle {
|
||||||
|
/// Positive/favorable hint (green)
|
||||||
|
case positive
|
||||||
|
/// Negative/warning hint (red)
|
||||||
|
case negative
|
||||||
|
/// Neutral/informational hint (yellow)
|
||||||
|
case neutral
|
||||||
|
/// Streak-based hint (orange/flame)
|
||||||
|
case streak
|
||||||
|
/// Choppy/alternating pattern (blue)
|
||||||
|
case pattern
|
||||||
|
/// Custom color
|
||||||
|
case custom(Color, String)
|
||||||
|
|
||||||
|
var color: Color {
|
||||||
|
switch self {
|
||||||
|
case .positive: return .green
|
||||||
|
case .negative: return .red
|
||||||
|
case .neutral: return .yellow
|
||||||
|
case .streak: return .orange
|
||||||
|
case .pattern: return .blue
|
||||||
|
case .custom(let color, _): return color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon: String {
|
||||||
|
switch self {
|
||||||
|
case .positive: return "arrow.up.circle.fill"
|
||||||
|
case .negative: return "arrow.down.circle.fill"
|
||||||
|
case .neutral: return "lightbulb.fill"
|
||||||
|
case .streak: return "flame.fill"
|
||||||
|
case .pattern: return "arrow.left.arrow.right"
|
||||||
|
case .custom(_, let icon): return icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A shared betting hint view for casino games.
|
||||||
|
/// Shows betting recommendations with consistent styling.
|
||||||
|
public struct BettingHintView: View {
|
||||||
|
/// The main hint text.
|
||||||
|
public let hint: String
|
||||||
|
|
||||||
|
/// Optional secondary info (e.g., trend percentage).
|
||||||
|
public let secondaryInfo: String?
|
||||||
|
|
||||||
|
/// The style of the hint (determines color and icon).
|
||||||
|
public let style: BettingHintStyle
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
/// Creates a betting hint view.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - hint: The main hint text.
|
||||||
|
/// - secondaryInfo: Optional secondary info text.
|
||||||
|
/// - style: The visual style of the hint.
|
||||||
|
public init(hint: String, secondaryInfo: String? = nil, style: BettingHintStyle = .neutral) {
|
||||||
|
self.hint = hint
|
||||||
|
self.secondaryInfo = secondaryInfo
|
||||||
|
self.style = style
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack(spacing: CasinoDesign.Spacing.small) {
|
||||||
|
Image(systemName: style.icon)
|
||||||
|
.font(.system(size: iconSize))
|
||||||
|
.foregroundStyle(style.color)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.xxSmall) {
|
||||||
|
Text(hint)
|
||||||
|
.font(.system(size: fontSize, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(CasinoDesign.MinScaleFactor.comfortable)
|
||||||
|
|
||||||
|
if let secondary = secondaryInfo {
|
||||||
|
Text(secondary)
|
||||||
|
.font(.system(size: fontSize - 2, weight: .regular))
|
||||||
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, paddingH)
|
||||||
|
.padding(.vertical, paddingV)
|
||||||
|
.frame(minWidth: CasinoDesign.HintSize.minWidth, alignment: .leading)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.black.opacity(CasinoDesign.Opacity.heavy))
|
||||||
|
.overlay(
|
||||||
|
Capsule()
|
||||||
|
.strokeBorder(style.color.opacity(CasinoDesign.Opacity.medium), lineWidth: CasinoDesign.LineWidth.thin)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: .black.opacity(CasinoDesign.Opacity.medium), radius: CasinoDesign.Shadow.radiusMedium)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(String(localized: "Betting Hint", bundle: .module))
|
||||||
|
.accessibilityValue(hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
#Preview("Neutral Hint") {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt.ignoresSafeArea()
|
||||||
|
BettingHintView(
|
||||||
|
hint: "Banker has the lowest house edge (1.06%)",
|
||||||
|
style: .neutral
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Positive Hint") {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt.ignoresSafeArea()
|
||||||
|
BettingHintView(
|
||||||
|
hint: "Bet 4x minimum",
|
||||||
|
secondaryInfo: "True count: +3",
|
||||||
|
style: .positive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Negative Hint") {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt.ignoresSafeArea()
|
||||||
|
BettingHintView(
|
||||||
|
hint: "Bet minimum",
|
||||||
|
secondaryInfo: "True count: -2",
|
||||||
|
style: .negative
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Streak Hint") {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt.ignoresSafeArea()
|
||||||
|
BettingHintView(
|
||||||
|
hint: "Banker streak: 5 in a row",
|
||||||
|
secondaryInfo: "B: 58% | P: 38%",
|
||||||
|
style: .streak
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Pattern Hint") {
|
||||||
|
ZStack {
|
||||||
|
Color.CasinoTable.felt.ignoresSafeArea()
|
||||||
|
BettingHintView(
|
||||||
|
hint: "Choppy shoe - results alternating",
|
||||||
|
style: .pattern
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user