Compare commits
No commits in common. "d16bf6eb2e35dd833f5627177b4f97606a46a97f" and "547f690e3c1b3a09178a7b40c882fac515618291" have entirely different histories.
d16bf6eb2e
...
547f690e3c
11
Agents.md
11
Agents.md
@ -360,17 +360,6 @@ Color.BettingZone.dragonBonusLight
|
|||||||
- If the project requires secrets such as API keys, never include them in the repository.
|
- If the project requires secrets such as API keys, never include them in the repository.
|
||||||
|
|
||||||
|
|
||||||
## Documentation instructions
|
|
||||||
|
|
||||||
- **Always keep each game's `README.md` file up to date** when adding new functionality or making changes that users or developers need to know about.
|
|
||||||
- Document new features, settings, or gameplay mechanics in the appropriate game's README.
|
|
||||||
- Update the README when modifying existing behavior that affects how the game works.
|
|
||||||
- Include any configuration options, keyboard shortcuts, or special interactions.
|
|
||||||
- If adding a new game to the workspace, create a comprehensive README following the existing games' format.
|
|
||||||
- README updates should be part of the same commit as the feature/change they document.
|
|
||||||
|
|
||||||
|
|
||||||
## PR instructions
|
## PR instructions
|
||||||
|
|
||||||
- If installed, make sure SwiftLint returns no warnings or errors before committing.
|
- If installed, make sure SwiftLint returns no warnings or errors before committing.
|
||||||
- Verify that the game's README.md reflects any new functionality or behavioral changes.
|
|
||||||
|
|||||||
@ -114,206 +114,6 @@ 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 {
|
||||||
@ -518,13 +318,6 @@ final class GameState {
|
|||||||
return bet.amount < minBet
|
return bet.amount < minBet
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which main bet is placed - nil if no main bet, true if Player, false if Banker.
|
|
||||||
/// Used to determine card layout ordering (betted hand appears on bottom).
|
|
||||||
var bettedOnPlayer: Bool? {
|
|
||||||
guard let bet = mainBet else { return nil }
|
|
||||||
return bet.type == .player
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Amount needed to reach the minimum bet.
|
/// Amount needed to reach the minimum bet.
|
||||||
var amountNeededForMinimum: Int {
|
var amountNeededForMinimum: Int {
|
||||||
guard let bet = mainBet else { return minBet }
|
guard let bet = mainBet else { return minBet }
|
||||||
@ -610,14 +403,8 @@ final class GameState {
|
|||||||
bankerHadPair = false
|
bankerHadPair = false
|
||||||
betResults = []
|
betResults = []
|
||||||
|
|
||||||
// Change to dealing phase - triggers layout animation (horizontal to vertical)
|
// Deal initial cards
|
||||||
currentPhase = .dealingInitial
|
currentPhase = .dealingInitial
|
||||||
|
|
||||||
// Wait for layout animation to complete before dealing cards
|
|
||||||
if settings.showAnimations {
|
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialCards = engine.dealInitialCards()
|
let initialCards = engine.dealInitialCards()
|
||||||
|
|
||||||
// Check if animations are enabled
|
// Check if animations are enabled
|
||||||
|
|||||||
@ -133,9 +133,6 @@ 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.
|
||||||
@ -157,7 +154,6 @@ 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"
|
||||||
@ -241,10 +237,6 @@ 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)
|
||||||
}
|
}
|
||||||
@ -292,10 +284,6 @@ 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)
|
||||||
}
|
}
|
||||||
@ -320,7 +308,6 @@ 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)
|
||||||
@ -334,7 +321,6 @@ 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)
|
||||||
@ -351,7 +337,6 @@ 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,42 +23,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"%@ 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" : {
|
||||||
@ -494,7 +458,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add $%lld more to meet minimum" : {
|
"Add $%lld more to meet minimum" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -721,9 +684,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"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.",
|
||||||
@ -931,10 +891,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -1004,14 +960,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -1034,9 +982,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Betting tips and trend analysis" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Blackjack" : {
|
"Blackjack" : {
|
||||||
"comment" : "The name of a blackjack game.",
|
"comment" : "The name of a blackjack game.",
|
||||||
@ -1222,10 +1167,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -1339,7 +1280,6 @@
|
|||||||
},
|
},
|
||||||
"Deal" : {
|
"Deal" : {
|
||||||
"comment" : "The label of a button that deals cards in a game.",
|
"comment" : "The label of a button that deals cards in a game.",
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1363,7 +1303,6 @@
|
|||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -1545,10 +1484,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -2577,14 +2512,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -2746,18 +2673,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -2804,10 +2719,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -2991,10 +2902,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -3132,9 +3039,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Show Hints" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Show History" : {
|
"Show History" : {
|
||||||
"comment" : "Toggle label for showing game history.",
|
"comment" : "Toggle label for showing game history.",
|
||||||
@ -3341,10 +3245,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Streak" : {
|
|
||||||
"comment" : "An accessibility label for the streak badge.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Sync Now" : {
|
"Sync Now" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -3389,10 +3289,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -3645,10 +3541,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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 = true
|
static let showDebugBorders = false
|
||||||
|
|
||||||
// MARK: - Shared Constants (from CasinoKit)
|
// MARK: - Shared Constants (from CasinoKit)
|
||||||
|
|
||||||
@ -28,8 +28,6 @@ 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)
|
||||||
|
|
||||||
@ -51,9 +49,6 @@ enum Design {
|
|||||||
// Labels
|
// Labels
|
||||||
static let labelFontSize: CGFloat = 14
|
static let labelFontSize: CGFloat = 14
|
||||||
static let labelRowHeight: CGFloat = 30
|
static let labelRowHeight: CGFloat = 30
|
||||||
|
|
||||||
// Buttons
|
|
||||||
static let bettingButtonsContainerHeight: CGFloat = 70
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Card Deal Animation
|
// MARK: - Card Deal Animation
|
||||||
|
|||||||
@ -28,7 +28,8 @@ struct ActionButtonsView: View {
|
|||||||
|
|
||||||
private let buttonFontSize: CGFloat = Design.BaseFontSize.xLarge
|
private let buttonFontSize: CGFloat = Design.BaseFontSize.xLarge
|
||||||
private let iconSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
|
private let iconSize: CGFloat = Design.BaseFontSize.xxLarge + Design.Spacing.xSmall
|
||||||
private let containerHeight: CGFloat = Design.Size.bettingButtonsContainerHeight
|
private let statusFontSize: CGFloat = Design.BaseFontSize.medium
|
||||||
|
private let containerHeight: CGFloat = 70
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
|
|
||||||
@ -44,6 +45,8 @@ struct ActionButtonsView: View {
|
|||||||
bettingButtons
|
bettingButtons
|
||||||
} else if gameState.currentPhase == .roundComplete && !gameState.showResultBanner {
|
} else if gameState.currentPhase == .roundComplete && !gameState.showResultBanner {
|
||||||
newRoundButton
|
newRoundButton
|
||||||
|
} else if !gameState.showResultBanner {
|
||||||
|
dealingIndicator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: gameState.currentPhase)
|
.animation(.easeInOut(duration: Design.Animation.quick), value: gameState.currentPhase)
|
||||||
@ -54,13 +57,104 @@ struct ActionButtonsView: View {
|
|||||||
// MARK: - Private Views
|
// MARK: - Private Views
|
||||||
|
|
||||||
private var bettingButtons: some View {
|
private var bettingButtons: some View {
|
||||||
BettingActionsView(
|
VStack(spacing: Design.Spacing.small) {
|
||||||
hasBet: !gameState.currentBets.isEmpty,
|
// Show hint if main bet is below minimum
|
||||||
canDeal: gameState.canDeal,
|
if gameState.isMainBetBelowMinimum {
|
||||||
amountNeededForMinimum: gameState.isMainBetBelowMinimum ? gameState.amountNeededForMinimum : nil,
|
Text(String(localized: "Add $\(gameState.amountNeededForMinimum) more to meet minimum"))
|
||||||
onClear: onClear,
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
||||||
onDeal: onDeal
|
.foregroundStyle(.orange)
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
clearButton
|
||||||
|
dealButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: Design.Animation.standard), value: gameState.isMainBetBelowMinimum)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dealingIndicator: some View {
|
||||||
|
HStack(spacing: Design.Spacing.xSmall) {
|
||||||
|
ProgressView()
|
||||||
|
.tint(.white)
|
||||||
|
.scaleEffect(0.8)
|
||||||
|
Text("Dealing...")
|
||||||
|
.font(.system(size: statusFontSize, weight: .medium))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(Design.MinScaleFactor.relaxed)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.xLarge)
|
||||||
|
.padding(.vertical, Design.Spacing.medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var clearButton: some View {
|
||||||
|
if isAccessibilitySize {
|
||||||
|
Button("Clear", systemImage: "xmark.circle", action: onClear)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.font(.system(size: iconSize, weight: .semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding(Design.Spacing.medium + Design.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(Color.CasinoButton.destructive)
|
||||||
|
)
|
||||||
|
.opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
|
||||||
|
.disabled(gameState.currentBets.isEmpty)
|
||||||
|
} else {
|
||||||
|
Button("Clear", systemImage: "xmark.circle", action: onClear)
|
||||||
|
.labelStyle(.titleAndIcon)
|
||||||
|
.font(.system(size: buttonFontSize, weight: .semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||||
|
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.CasinoButton.destructive)
|
||||||
|
)
|
||||||
|
.opacity(gameState.currentBets.isEmpty ? Design.Opacity.disabled : 1.0)
|
||||||
|
.disabled(gameState.currentBets.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var dealButton: some View {
|
||||||
|
let buttonBackground = LinearGradient(
|
||||||
|
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if isAccessibilitySize {
|
||||||
|
Button("Deal", systemImage: "play.fill", action: onDeal)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.font(.system(size: iconSize, weight: .bold))
|
||||||
|
.foregroundStyle(.black)
|
||||||
|
.padding(Design.Spacing.medium + Design.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(buttonBackground)
|
||||||
|
)
|
||||||
|
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
|
||||||
|
.opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
|
||||||
|
.disabled(!gameState.canDeal)
|
||||||
|
} else {
|
||||||
|
Button("Deal", systemImage: "play.fill", action: onDeal)
|
||||||
|
.labelStyle(.titleAndIcon)
|
||||||
|
.font(.system(size: buttonFontSize, weight: .bold))
|
||||||
|
.foregroundStyle(.black)
|
||||||
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||||
|
.padding(.vertical, Design.Spacing.medium + Design.Spacing.xxSmall)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(buttonBackground)
|
||||||
|
)
|
||||||
|
.shadow(color: .yellow.opacity(Design.Opacity.light), radius: Design.Shadow.radiusMedium)
|
||||||
|
.opacity(gameState.canDeal ? 1.0 : Design.Opacity.disabled)
|
||||||
|
.disabled(!gameState.canDeal)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@ -17,9 +17,6 @@ struct GameTableView: View {
|
|||||||
@State private var showRules = false
|
@State private var showRules = false
|
||||||
@State private var showStats = false
|
@State private var showStats = false
|
||||||
|
|
||||||
/// Screen size for card sizing (measured from TableBackgroundView)
|
|
||||||
@State private var screenSize: CGSize = CGSize(width: 375, height: 667)
|
|
||||||
|
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
|
||||||
@ -40,21 +37,26 @@ struct GameTableView: View {
|
|||||||
isLandscape ? Design.Spacing.medium : Design.Spacing.xSmall
|
isLandscape ? Design.Spacing.medium : Design.Spacing.xSmall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimum spacer height - smaller in landscape to fit content
|
||||||
|
private var minSpacerHeight: CGFloat {
|
||||||
|
isLandscape ? 0 : Design.Spacing.xSmall
|
||||||
|
}
|
||||||
|
|
||||||
/// Smaller spacer height - reduced in landscape
|
/// Smaller spacer height - reduced in landscape
|
||||||
private var smallSpacerHeight: CGFloat {
|
private var smallSpacerHeight: CGFloat {
|
||||||
isLandscape ? Design.Spacing.xxSmall : Design.Spacing.small
|
isLandscape ? Design.Spacing.xxSmall : Design.Spacing.small
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Medium spacer height - reduced in landscape
|
||||||
|
private var mediumSpacerHeight: CGFloat {
|
||||||
|
isLandscape ? Design.Spacing.xSmall : Design.Spacing.medium
|
||||||
|
}
|
||||||
|
|
||||||
/// Maximum width for game content on large screens
|
/// Maximum width for game content on large screens
|
||||||
private var maxContentWidth: CGFloat {
|
private var maxContentWidth: CGFloat {
|
||||||
isLandscape ? CasinoDesign.Size.maxContentWidthLandscape : CasinoDesign.Size.maxContentWidthPortrait
|
isLandscape ? CasinoDesign.Size.maxContentWidthLandscape : CasinoDesign.Size.maxContentWidthPortrait
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether we're on a small screen (like iPhone SE) where space is tight
|
|
||||||
private var isSmallScreen: Bool {
|
|
||||||
screenSize.height < 700
|
|
||||||
}
|
|
||||||
|
|
||||||
private var state: GameState {
|
private var state: GameState {
|
||||||
gameState ?? GameState(settings: settings)
|
gameState ?? GameState(settings: settings)
|
||||||
}
|
}
|
||||||
@ -71,11 +73,6 @@ struct GameTableView: View {
|
|||||||
state.lastResult == .tie
|
state.lastResult == .tie
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether we're in a dealing/result phase (vertical layout) vs betting phase (horizontal)
|
|
||||||
private var isDealing: Bool {
|
|
||||||
state.currentPhase != .betting
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 }
|
||||||
|
|
||||||
@ -84,13 +81,8 @@ struct GameTableView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
// Table background - measures screen size for card sizing
|
// Table background (from CasinoKit)
|
||||||
TableBackgroundView()
|
TableBackgroundView()
|
||||||
.onGeometryChange(for: CGSize.self) { proxy in
|
|
||||||
proxy.size
|
|
||||||
} action: { size in
|
|
||||||
screenSize = size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main content
|
// Main content
|
||||||
mainContent(geometry: geometry)
|
mainContent(geometry: geometry)
|
||||||
@ -203,10 +195,7 @@ struct GameTableView: View {
|
|||||||
bankerIsWinner: bankerIsWinner,
|
bankerIsWinner: bankerIsWinner,
|
||||||
isTie: isTie,
|
isTie: isTie,
|
||||||
showAnimations: settings.showAnimations,
|
showAnimations: settings.showAnimations,
|
||||||
dealingSpeed: settings.dealingSpeed,
|
dealingSpeed: settings.dealingSpeed
|
||||||
bettedOnPlayer: state.bettedOnPlayer,
|
|
||||||
isDealing: isDealing,
|
|
||||||
screenSize: screenSize
|
|
||||||
)
|
)
|
||||||
.frame(maxWidth: maxContentWidth)
|
.frame(maxWidth: maxContentWidth)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
@ -223,62 +212,43 @@ 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)
|
Spacer(minLength: Design.Spacing.xSmall)
|
||||||
if let hintInfo = state.currentHintInfo {
|
|
||||||
BettingHintView(
|
// Chip selector - full width so all chips are tappable
|
||||||
hint: hintInfo.text,
|
ChipSelectorView(
|
||||||
secondaryInfo: hintInfo.secondaryText,
|
selectedChip: $selectedChip,
|
||||||
style: hintInfo.style
|
balance: state.balance,
|
||||||
)
|
currentBet: state.totalBetAmount,
|
||||||
.transition(.opacity)
|
maxBet: state.maxBet
|
||||||
.padding(.vertical, Design.Spacing.small)
|
)
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: Design.Spacing.xSmall)
|
Spacer(minLength: Design.Spacing.xSmall)
|
||||||
|
|
||||||
// Chip selector - only shown during betting phase
|
// Action buttons
|
||||||
if state.currentPhase == .betting && !state.showResultBanner {
|
ActionButtonsView(
|
||||||
ChipSelectorView(
|
gameState: state,
|
||||||
selectedChip: $selectedChip,
|
onDeal: {
|
||||||
balance: state.balance,
|
Task {
|
||||||
currentBet: state.totalBetAmount,
|
await state.deal()
|
||||||
maxBet: state.maxBet
|
}
|
||||||
)
|
},
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
onClear: { state.clearBets() },
|
||||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
onNewRound: { state.newRound() }
|
||||||
}
|
)
|
||||||
|
.frame(maxWidth: maxContentWidth * 0.8)
|
||||||
Spacer(minLength: Design.Spacing.xSmall)
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom, Design.Spacing.small)
|
||||||
// Action buttons - hidden on small screens during dealing to save space
|
.debugBorder(showDebugBorders, color: .green, label: "ActionBtns")
|
||||||
if !isDealing {
|
|
||||||
// Action buttons
|
|
||||||
ActionButtonsView(
|
|
||||||
gameState: state,
|
|
||||||
onDeal: {
|
|
||||||
Task {
|
|
||||||
await state.deal()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClear: { state.clearBets() },
|
|
||||||
onNewRound: { state.newRound() }
|
|
||||||
)
|
|
||||||
.frame(maxWidth: maxContentWidth * 0.8)
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.bottom, Design.Spacing.small)
|
|
||||||
.debugBorder(showDebugBorders, color: .green, label: "ActionBtns")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: state.currentPhase)
|
|
||||||
.debugBorder(showDebugBorders, color: .white, label: "GameContent")
|
.debugBorder(showDebugBorders, color: .white, label: "GameContent")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.safeAreaPadding(.bottom, Design.Spacing.small)
|
.safeAreaPadding(.bottom, Design.Spacing.small)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Portrait layout - vertical card layout, no RoadMap (shown only in landscape)
|
/// Portrait layout with RoadMap inline
|
||||||
private var portraitLayout: some View {
|
private var portraitLayout: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Top bar with balance and info (from CasinoKit)
|
// Top bar with balance and info (from CasinoKit)
|
||||||
@ -292,7 +262,10 @@ struct GameTableView: View {
|
|||||||
)
|
)
|
||||||
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
|
||||||
|
|
||||||
// Cards display area - animates from horizontal to vertical when dealing
|
Spacer(minLength: minSpacerHeight)
|
||||||
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer1")
|
||||||
|
|
||||||
|
// Cards display area
|
||||||
CardsDisplayArea(
|
CardsDisplayArea(
|
||||||
playerCards: state.visiblePlayerCards,
|
playerCards: state.visiblePlayerCards,
|
||||||
bankerCards: state.visibleBankerCards,
|
bankerCards: state.visibleBankerCards,
|
||||||
@ -304,29 +277,26 @@ struct GameTableView: View {
|
|||||||
bankerIsWinner: bankerIsWinner,
|
bankerIsWinner: bankerIsWinner,
|
||||||
isTie: isTie,
|
isTie: isTie,
|
||||||
showAnimations: settings.showAnimations,
|
showAnimations: settings.showAnimations,
|
||||||
dealingSpeed: settings.dealingSpeed,
|
dealingSpeed: settings.dealingSpeed
|
||||||
bettedOnPlayer: state.bettedOnPlayer,
|
|
||||||
isDealing: isDealing,
|
|
||||||
screenSize: screenSize
|
|
||||||
)
|
)
|
||||||
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.debugBorder(showDebugBorders, color: .red, label: "CardsArea")
|
.debugBorder(showDebugBorders, color: .red, label: "CardsArea")
|
||||||
|
|
||||||
Spacer(minLength: smallSpacerHeight)
|
Spacer(minLength: minSpacerHeight)
|
||||||
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer2")
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer2")
|
||||||
|
|
||||||
// Road map history - show in portrait before deal on larger screens
|
// Road map history
|
||||||
if settings.showHistory && !isSmallScreen && !isDealing {
|
if settings.showHistory {
|
||||||
RoadMapView(results: state.recentResults)
|
RoadMapView(results: state.recentResults)
|
||||||
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.debugBorder(showDebugBorders, color: .orange, label: "RoadMap")
|
.debugBorder(showDebugBorders, color: .orange, label: "RoadMap")
|
||||||
|
|
||||||
Spacer(minLength: smallSpacerHeight)
|
|
||||||
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer3")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: smallSpacerHeight)
|
||||||
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer3")
|
||||||
|
|
||||||
// Betting table
|
// Betting table
|
||||||
BettingTableView(
|
BettingTableView(
|
||||||
gameState: state,
|
gameState: state,
|
||||||
@ -336,52 +306,37 @@ 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)
|
Spacer(minLength: mediumSpacerHeight)
|
||||||
if let hintInfo = state.currentHintInfo {
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer4")
|
||||||
BettingHintView(
|
|
||||||
hint: hintInfo.text,
|
|
||||||
secondaryInfo: hintInfo.secondaryText,
|
|
||||||
style: hintInfo.style
|
|
||||||
)
|
|
||||||
.transition(.opacity)
|
|
||||||
.padding(.vertical, Design.Spacing.small)
|
|
||||||
.debugBorder(showDebugBorders, color: .purple, label: "Hint")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chip selector - only shown during betting phase
|
// Chip selector - full width so all chips are tappable
|
||||||
if state.currentPhase == .betting && !state.showResultBanner {
|
ChipSelectorView(
|
||||||
ChipSelectorView(
|
selectedChip: $selectedChip,
|
||||||
selectedChip: $selectedChip,
|
balance: state.balance,
|
||||||
balance: state.balance,
|
currentBet: state.totalBetAmount,
|
||||||
currentBet: state.totalBetAmount,
|
maxBet: state.maxBet
|
||||||
maxBet: state.maxBet
|
)
|
||||||
)
|
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
|
||||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action buttons - hidden on small screens during dealing to save space
|
Spacer(minLength: smallSpacerHeight)
|
||||||
if isSmallScreen && isDealing {
|
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer5")
|
||||||
Spacer()
|
|
||||||
.frame(height: 5)
|
// Action buttons
|
||||||
} else {
|
ActionButtonsView(
|
||||||
ActionButtonsView(
|
gameState: state,
|
||||||
gameState: state,
|
onDeal: {
|
||||||
onDeal: {
|
Task {
|
||||||
Task {
|
await state.deal()
|
||||||
await state.deal()
|
}
|
||||||
}
|
},
|
||||||
},
|
onClear: { state.clearBets() },
|
||||||
onClear: { state.clearBets() },
|
onNewRound: { state.newRound() }
|
||||||
onNewRound: { state.newRound() }
|
)
|
||||||
)
|
.frame(maxWidth: isLargeScreen ? maxContentWidth * 0.8 : .infinity)
|
||||||
.frame(maxWidth: isLargeScreen ? maxContentWidth * 0.8 : .infinity)
|
.padding(.horizontal)
|
||||||
.padding(.horizontal)
|
.padding(.bottom, bottomPadding)
|
||||||
.padding(.bottom, bottomPadding)
|
.debugBorder(showDebugBorders, color: .green, label: "ActionBtns")
|
||||||
.debugBorder(showDebugBorders, color: .green, label: "ActionBtns")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: state.currentPhase)
|
|
||||||
.safeAreaPadding(.bottom)
|
.safeAreaPadding(.bottom)
|
||||||
.debugBorder(showDebugBorders, color: .white, label: "MainContent")
|
.debugBorder(showDebugBorders, color: .white, label: "MainContent")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,16 +92,6 @@ 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
|
||||||
|
|||||||
@ -3,15 +3,12 @@
|
|||||||
// Baccarat
|
// Baccarat
|
||||||
//
|
//
|
||||||
// The cards display area showing both Player and Banker hands.
|
// The cards display area showing both Player and Banker hands.
|
||||||
// Animates from side-by-side (betting) to vertical stack (dealing).
|
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CasinoKit
|
import CasinoKit
|
||||||
|
|
||||||
/// The cards display area showing both hands.
|
/// The cards display area showing both hands.
|
||||||
/// - Betting phase: Horizontal side-by-side layout (Player | Banker)
|
|
||||||
/// - Dealing phase: Vertical stack with betted hand on bottom
|
|
||||||
struct CardsDisplayArea: View {
|
struct CardsDisplayArea: View {
|
||||||
let playerCards: [Card]
|
let playerCards: [Card]
|
||||||
let bankerCards: [Card]
|
let bankerCards: [Card]
|
||||||
@ -24,87 +21,38 @@ struct CardsDisplayArea: View {
|
|||||||
let isTie: Bool
|
let isTie: Bool
|
||||||
let showAnimations: Bool
|
let showAnimations: Bool
|
||||||
let dealingSpeed: Double
|
let dealingSpeed: Double
|
||||||
/// Which main bet is placed - nil if no main bet, true if Player, false if Banker.
|
|
||||||
let bettedOnPlayer: Bool?
|
|
||||||
/// Whether the game is in dealing/result phase (vertical layout) or betting phase (horizontal).
|
|
||||||
let isDealing: Bool
|
|
||||||
/// Full screen size for calculating card width in dealing mode.
|
|
||||||
let screenSize: CGSize
|
|
||||||
|
|
||||||
// MARK: - State
|
// MARK: - State
|
||||||
|
|
||||||
@State private var containerWidth: CGFloat = 300
|
@State private var containerWidth: CGFloat = 300
|
||||||
@Namespace private var animation
|
|
||||||
|
|
||||||
// MARK: - Environment
|
// MARK: - Environment
|
||||||
|
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
|
/// Whether we're on a large screen (iPad)
|
||||||
private var isLargeScreen: Bool {
|
private var isLargeScreen: Bool {
|
||||||
horizontalSizeClass == .regular
|
horizontalSizeClass == .regular
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether we're in landscape mode
|
// Use global debug flag from Design constants
|
||||||
private var isLandscape: Bool {
|
|
||||||
verticalSizeClass == .compact || (isLargeScreen && screenSize.width > screenSize.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
private var showDebugBorders: Bool { Design.showDebugBorders }
|
||||||
|
|
||||||
|
/// Label font size - larger on iPad
|
||||||
private var labelFontSize: CGFloat {
|
private var labelFontSize: CGFloat {
|
||||||
isLargeScreen ? 18 : Design.Size.labelFontSize
|
isLargeScreen ? 18 : Design.Size.labelFontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimum height for label row - larger on iPad
|
||||||
private var labelRowMinHeight: CGFloat {
|
private var labelRowMinHeight: CGFloat {
|
||||||
isLargeScreen ? 40 : Design.Size.labelRowHeight
|
isLargeScreen ? 40 : Design.Size.labelRowHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether Player hand should be on bottom in vertical mode.
|
/// Spacing between PLAYER and BANKER hands - reduced on smaller screens
|
||||||
private var playerOnBottom: Bool {
|
|
||||||
bettedOnPlayer ?? true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spacing between hands
|
|
||||||
private var handsSpacing: CGFloat {
|
private var handsSpacing: CGFloat {
|
||||||
if isDealing {
|
isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large
|
||||||
return isLargeScreen ? Design.Spacing.large : Design.Spacing.medium
|
|
||||||
} else {
|
|
||||||
return isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Card Width Calculations
|
|
||||||
|
|
||||||
/// Card section width for horizontal (betting) mode
|
|
||||||
private var horizontalHandWidth: CGFloat {
|
|
||||||
let spacing = isLargeScreen ? Design.Spacing.xxxLarge : Design.Spacing.large
|
|
||||||
return max(100, (containerWidth - spacing) / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Card section width for vertical (dealing) mode - matches Blackjack sizing
|
|
||||||
/// Uses screen height as base (like Blackjack), which is smaller in landscape
|
|
||||||
private var verticalHandWidth: CGFloat {
|
|
||||||
// Use screen height (smaller dimension in landscape = smaller cards)
|
|
||||||
let height = screenSize.height
|
|
||||||
guard height > 100 else { return horizontalHandWidth }
|
|
||||||
|
|
||||||
// Blackjack uses 0.18, but in landscape we may need slightly smaller
|
|
||||||
let percentage: CGFloat = isLandscape ? 0.175 : height < 700 ? 0.14 : 0.18
|
|
||||||
let cardWidth = height * percentage
|
|
||||||
|
|
||||||
// CompactHandView: cardWidth = containerWidth / divisor
|
|
||||||
let overlapRatio: CGFloat = -0.45
|
|
||||||
let maxCards: CGFloat = 3
|
|
||||||
let divisor = 1 + (maxCards - 1) * (1 + overlapRatio)
|
|
||||||
return cardWidth * divisor
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current hand section width based on mode
|
|
||||||
private var handSectionWidth: CGFloat {
|
|
||||||
isDealing ? verticalHandWidth : horizontalHandWidth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Accessibility
|
// MARK: - Accessibility
|
||||||
@ -141,43 +89,22 @@ struct CardsDisplayArea: View {
|
|||||||
return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue)
|
return visibleCards.joined(separator: ", ") + ". " + String(format: format, bankerValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate hand section width from total container width
|
||||||
|
private var handSectionWidth: CGFloat {
|
||||||
|
(containerWidth - handsSpacing) / 2
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// Use different layouts but keep view identity with matchedGeometryEffect
|
HStack(spacing: handsSpacing) {
|
||||||
Group {
|
// Player side
|
||||||
if isDealing {
|
playerHandSection(width: handSectionWidth)
|
||||||
// Vertical layout with flexible spacing between hands
|
.debugBorder(showDebugBorders, color: .blue, label: "Player")
|
||||||
VStack(spacing: 0) {
|
|
||||||
if playerOnBottom {
|
|
||||||
bankerHandSection(width: handSectionWidth)
|
|
||||||
.matchedGeometryEffect(id: "banker", in: animation)
|
|
||||||
|
|
||||||
// Flexible spacer expands on taller screens
|
// Banker side
|
||||||
Spacer(minLength: handsSpacing)
|
bankerHandSection(width: handSectionWidth)
|
||||||
|
.debugBorder(showDebugBorders, color: .red, label: "Banker")
|
||||||
playerHandSection(width: handSectionWidth)
|
|
||||||
.matchedGeometryEffect(id: "player", in: animation)
|
|
||||||
} else {
|
|
||||||
playerHandSection(width: handSectionWidth)
|
|
||||||
.matchedGeometryEffect(id: "player", in: animation)
|
|
||||||
|
|
||||||
// Flexible spacer expands on taller screens
|
|
||||||
Spacer(minLength: handsSpacing)
|
|
||||||
|
|
||||||
bankerHandSection(width: handSectionWidth)
|
|
||||||
.matchedGeometryEffect(id: "banker", in: animation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Horizontal layout - Player left, Banker right
|
|
||||||
HStack(spacing: handsSpacing) {
|
|
||||||
playerHandSection(width: handSectionWidth)
|
|
||||||
.matchedGeometryEffect(id: "player", in: animation)
|
|
||||||
bankerHandSection(width: handSectionWidth)
|
|
||||||
.matchedGeometryEffect(id: "banker", in: animation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.top, Design.Spacing.medium)
|
.padding(.top, Design.Spacing.medium)
|
||||||
@ -199,14 +126,13 @@ struct CardsDisplayArea: View {
|
|||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
)
|
)
|
||||||
.debugBorder(showDebugBorders, color: .mint, label: "HandsContainer")
|
.debugBorder(showDebugBorders, color: .mint, label: "HandsContainer")
|
||||||
.animation(.spring(duration: 0.6, bounce: 0.2), value: isDealing)
|
|
||||||
.animation(.spring(duration: 0.5, bounce: 0.15), value: playerOnBottom)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Views
|
// MARK: - Private Views
|
||||||
|
|
||||||
private func playerHandSection(width: CGFloat) -> some View {
|
private func playerHandSection(width: CGFloat) -> some View {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
|
// Label with value
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Text("PLAYER")
|
Text("PLAYER")
|
||||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||||
@ -218,6 +144,7 @@ struct CardsDisplayArea: View {
|
|||||||
}
|
}
|
||||||
.frame(minHeight: labelRowMinHeight)
|
.frame(minHeight: labelRowMinHeight)
|
||||||
|
|
||||||
|
// Cards
|
||||||
CompactHandView(
|
CompactHandView(
|
||||||
cards: playerCards,
|
cards: playerCards,
|
||||||
cardsFaceUp: playerCardsFaceUp,
|
cardsFaceUp: playerCardsFaceUp,
|
||||||
@ -235,6 +162,7 @@ struct CardsDisplayArea: View {
|
|||||||
|
|
||||||
private func bankerHandSection(width: CGFloat) -> some View {
|
private func bankerHandSection(width: CGFloat) -> some View {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
|
// Label with value
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Text("BANKER")
|
Text("BANKER")
|
||||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||||
@ -246,6 +174,7 @@ struct CardsDisplayArea: View {
|
|||||||
}
|
}
|
||||||
.frame(minHeight: labelRowMinHeight)
|
.frame(minHeight: labelRowMinHeight)
|
||||||
|
|
||||||
|
// Cards
|
||||||
CompactHandView(
|
CompactHandView(
|
||||||
cards: bankerCards,
|
cards: bankerCards,
|
||||||
cardsFaceUp: bankerCardsFaceUp,
|
cardsFaceUp: bankerCardsFaceUp,
|
||||||
@ -264,7 +193,7 @@ struct CardsDisplayArea: View {
|
|||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
#Preview("Horizontal - Betting Phase") {
|
#Preview("Empty Hands") {
|
||||||
ZStack {
|
ZStack {
|
||||||
TableBackgroundView()
|
TableBackgroundView()
|
||||||
CardsDisplayArea(
|
CardsDisplayArea(
|
||||||
@ -278,15 +207,12 @@ struct CardsDisplayArea: View {
|
|||||||
bankerIsWinner: false,
|
bankerIsWinner: false,
|
||||||
isTie: false,
|
isTie: false,
|
||||||
showAnimations: true,
|
showAnimations: true,
|
||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0
|
||||||
bettedOnPlayer: nil,
|
|
||||||
isDealing: false,
|
|
||||||
screenSize: CGSize(width: 400, height: 800)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Vertical - Dealing (Bet on Player)") {
|
#Preview("Player Wins") {
|
||||||
ZStack {
|
ZStack {
|
||||||
TableBackgroundView()
|
TableBackgroundView()
|
||||||
CardsDisplayArea(
|
CardsDisplayArea(
|
||||||
@ -306,15 +232,12 @@ struct CardsDisplayArea: View {
|
|||||||
bankerIsWinner: false,
|
bankerIsWinner: false,
|
||||||
isTie: false,
|
isTie: false,
|
||||||
showAnimations: true,
|
showAnimations: true,
|
||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0
|
||||||
bettedOnPlayer: true,
|
|
||||||
isDealing: true,
|
|
||||||
screenSize: CGSize(width: 400, height: 800)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Vertical - Dealing (Bet on Banker)") {
|
#Preview("Banker Wins with 3 Cards") {
|
||||||
ZStack {
|
ZStack {
|
||||||
TableBackgroundView()
|
TableBackgroundView()
|
||||||
CardsDisplayArea(
|
CardsDisplayArea(
|
||||||
@ -331,15 +254,13 @@ struct CardsDisplayArea: View {
|
|||||||
playerCardsFaceUp: [true, true, true],
|
playerCardsFaceUp: [true, true, true],
|
||||||
bankerCardsFaceUp: [true, true, true],
|
bankerCardsFaceUp: [true, true, true],
|
||||||
playerValue: 9,
|
playerValue: 9,
|
||||||
bankerValue: 9,
|
bankerValue: 8,
|
||||||
playerIsWinner: false,
|
playerIsWinner: false,
|
||||||
bankerIsWinner: true,
|
bankerIsWinner: true,
|
||||||
isTie: false,
|
isTie: false,
|
||||||
showAnimations: true,
|
showAnimations: true,
|
||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0
|
||||||
bettedOnPlayer: false,
|
|
||||||
isDealing: true,
|
|
||||||
screenSize: CGSize(width: 400, height: 800)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,195 +0,0 @@
|
|||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -47,7 +47,7 @@ enum HandResult: Equatable {
|
|||||||
|
|
||||||
var color: Color {
|
var color: Color {
|
||||||
switch self {
|
switch self {
|
||||||
case .blackjack: return Color(red: 0.85, green: 0.55, blue: 0.1) // Dark amber - good contrast with white text
|
case .blackjack: return .yellow
|
||||||
case .win: return .green
|
case .win: return .green
|
||||||
case .lose, .bust, .insuranceLose: return .red
|
case .lose, .bust, .insuranceLose: return .red
|
||||||
case .push: return .blue
|
case .push: return .blue
|
||||||
|
|||||||
@ -804,7 +804,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add $%lld more to meet minimum" : {
|
"Add $%lld more to meet minimum" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -1456,7 +1455,6 @@
|
|||||||
},
|
},
|
||||||
"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" : {
|
||||||
@ -2220,7 +2218,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Deal" : {
|
"Deal" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
@ -11,6 +11,10 @@ import CasinoKit
|
|||||||
struct ActionButtonsView: View {
|
struct ActionButtonsView: View {
|
||||||
@Bindable var state: GameState
|
@Bindable var state: GameState
|
||||||
|
|
||||||
|
// Scaled metrics
|
||||||
|
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large
|
||||||
|
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large
|
||||||
|
|
||||||
// Scaled container height - base 60pt, scales with accessibility
|
// Scaled container height - base 60pt, scales with accessibility
|
||||||
@ScaledMetric(relativeTo: .body) private var containerHeight: CGFloat = 60
|
@ScaledMetric(relativeTo: .body) private var containerHeight: CGFloat = 60
|
||||||
|
|
||||||
@ -21,10 +25,8 @@ struct ActionButtonsView: View {
|
|||||||
switch state.currentPhase {
|
switch state.currentPhase {
|
||||||
case .betting:
|
case .betting:
|
||||||
bettingButtons
|
bettingButtons
|
||||||
.transition(.opacity.combined(with: .scale(scale: 0.9)))
|
|
||||||
case .playerTurn:
|
case .playerTurn:
|
||||||
playerTurnButtons
|
playerTurnButtons
|
||||||
.transition(.opacity.combined(with: .scale(scale: 0.9)))
|
|
||||||
case .roundComplete:
|
case .roundComplete:
|
||||||
// Empty - handled by result banner
|
// Empty - handled by result banner
|
||||||
EmptyView()
|
EmptyView()
|
||||||
@ -33,7 +35,6 @@ struct ActionButtonsView: View {
|
|||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
|
|
||||||
}
|
}
|
||||||
.frame(height: containerHeight)
|
.frame(height: containerHeight)
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.padding(.horizontal, Design.Spacing.large)
|
||||||
@ -43,97 +44,92 @@ struct ActionButtonsView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var bettingButtons: some View {
|
private var bettingButtons: some View {
|
||||||
BettingActionsView(
|
if state.currentBet > 0 {
|
||||||
hasBet: state.currentBet > 0,
|
VStack(spacing: Design.Spacing.small) {
|
||||||
canDeal: state.canDeal,
|
// Show hint if bet is below minimum
|
||||||
amountNeededForMinimum: state.isBetBelowMinimum ? state.amountNeededForMinimum : nil,
|
if state.isBetBelowMinimum {
|
||||||
onClear: { state.clearBet() },
|
Text(String(localized: "Add $\(state.amountNeededForMinimum) more to meet minimum"))
|
||||||
onDeal: { Task { await state.deal() } }
|
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
||||||
)
|
.foregroundStyle(.orange)
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
ActionButton(
|
||||||
|
String(localized: "Clear"),
|
||||||
|
icon: "xmark.circle",
|
||||||
|
style: .destructive
|
||||||
|
) {
|
||||||
|
state.clearBet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show Deal button, but disable if below minimum
|
||||||
|
ActionButton(
|
||||||
|
String(localized: "Deal"),
|
||||||
|
icon: "play.fill",
|
||||||
|
style: .primary
|
||||||
|
) {
|
||||||
|
Task { await state.deal() }
|
||||||
|
}
|
||||||
|
.opacity(state.canDeal ? 1.0 : Design.Opacity.medium)
|
||||||
|
.disabled(!state.canDeal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.9)))
|
||||||
|
.animation(.easeInOut(duration: Design.Animation.standard), value: state.isBetBelowMinimum)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Player Turn Buttons
|
// MARK: - Player Turn Buttons
|
||||||
|
|
||||||
/// Available player actions based on current game state.
|
|
||||||
private var availableActions: [PlayerAction] {
|
|
||||||
PlayerAction.allCases.filter { action in
|
|
||||||
switch action {
|
|
||||||
case .hit: state.canHit
|
|
||||||
case .stand: state.canStand
|
|
||||||
case .double: state.canDouble
|
|
||||||
case .split: state.canSplit
|
|
||||||
case .surrender: state.canSurrender
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tracks available actions for animation purposes.
|
|
||||||
@State private var animatedActions: [PlayerAction] = []
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var playerTurnButtons: some View {
|
private var playerTurnButtons: some View {
|
||||||
|
// All player actions in a single row
|
||||||
HStack(spacing: Design.Spacing.medium) {
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
ForEach(animatedActions) { action in
|
if state.canHit {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
action.title,
|
String(localized: "Hit"),
|
||||||
style: .custom(action.color)
|
style: .custom(Color.Button.hit)
|
||||||
) {
|
) {
|
||||||
Task { await performAction(action) }
|
Task { await state.hit() }
|
||||||
}
|
}
|
||||||
.transition(.scale.combined(with: .opacity))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.onAppear {
|
if state.canStand {
|
||||||
animatedActions = availableActions
|
ActionButton(
|
||||||
}
|
String(localized: "Stand"),
|
||||||
.onChange(of: availableActions) { _, newActions in
|
style: .custom(Color.Button.stand)
|
||||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: 0.15)) {
|
) {
|
||||||
animatedActions = newActions
|
Task { await state.stand() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs the given player action.
|
if state.canDouble {
|
||||||
private func performAction(_ action: PlayerAction) async {
|
ActionButton(
|
||||||
switch action {
|
String(localized: "Double"),
|
||||||
case .hit: await state.hit()
|
style: .custom(Color.Button.doubleDown)
|
||||||
case .stand: await state.stand()
|
) {
|
||||||
case .double: await state.doubleDown()
|
Task { await state.doubleDown() }
|
||||||
case .split: await state.split()
|
}
|
||||||
case .surrender: await state.surrender()
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Player Action
|
if state.canSplit {
|
||||||
|
ActionButton(
|
||||||
|
String(localized: "Split"),
|
||||||
|
style: .custom(Color.Button.split)
|
||||||
|
) {
|
||||||
|
Task { await state.split() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a player action button during their turn.
|
if state.canSurrender {
|
||||||
private enum PlayerAction: String, CaseIterable, Identifiable {
|
ActionButton(
|
||||||
case hit
|
String(localized: "Surrender"),
|
||||||
case stand
|
style: .custom(Color.Button.surrender)
|
||||||
case double
|
) {
|
||||||
case split
|
Task { await state.surrender() }
|
||||||
case surrender
|
}
|
||||||
|
}
|
||||||
var id: String { rawValue }
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .hit: String(localized: "Hit")
|
|
||||||
case .stand: String(localized: "Stand")
|
|
||||||
case .double: String(localized: "Double")
|
|
||||||
case .split: String(localized: "Split")
|
|
||||||
case .surrender: String(localized: "Surrender")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch self {
|
|
||||||
case .hit: Color.Button.hit
|
|
||||||
case .stand: Color.Button.stand
|
|
||||||
case .double: Color.Button.doubleDown
|
|
||||||
case .split: Color.Button.split
|
|
||||||
case .surrender: Color.Button.surrender
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,7 +156,6 @@ struct GameTableView: View {
|
|||||||
currentBet: state.minBetForChipSelector,
|
currentBet: state.minBetForChipSelector,
|
||||||
maxBet: state.settings.maxBet
|
maxBet: state.settings.maxBet
|
||||||
)
|
)
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
|
||||||
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +166,6 @@ struct GameTableView: View {
|
|||||||
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
|
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: state.currentPhase)
|
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
.onChange(of: state.currentPhase) { oldPhase, newPhase in
|
||||||
Design.debugLog("🔄 Phase changed: \(oldPhase) → \(newPhase)")
|
Design.debugLog("🔄 Phase changed: \(oldPhase) → \(newPhase)")
|
||||||
|
|||||||
@ -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 {
|
||||||
BlackjackBettingHintView(hint: hint, trueCount: state.engine.trueCount)
|
BettingHintView(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,28 +48,64 @@ struct HintView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Blackjack Betting Hint View
|
// MARK: - Betting Hint View
|
||||||
|
|
||||||
/// Shows betting recommendations based on the current card count.
|
/// Shows betting recommendations based on the current card count.
|
||||||
/// Maps true count to hint style and uses CasinoKit's shared BettingHintView.
|
struct BettingHintView: View {
|
||||||
struct BlackjackBettingHintView: View {
|
|
||||||
let hint: String
|
let hint: String
|
||||||
let trueCount: Double
|
let trueCount: Double
|
||||||
|
|
||||||
/// Determines the hint style based on true count.
|
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.Size.hintIconSize
|
||||||
private var style: BettingHintStyle {
|
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = Design.Size.hintFontSize
|
||||||
|
@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 .positive // Player advantage - bet more
|
return .green // Player advantage - bet more
|
||||||
} else if tc <= -1 {
|
} else if tc <= -1 {
|
||||||
return .negative // House advantage - bet less
|
return .red // House advantage - bet less
|
||||||
} else {
|
} else {
|
||||||
return .neutral // Neutral
|
return .yellow // 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 {
|
||||||
BettingHintView(hint: hint, style: style)
|
HStack(spacing: Design.Spacing.small) {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,21 +128,21 @@ struct BlackjackBettingHintView: View {
|
|||||||
#Preview("Betting Hint - Positive Count") {
|
#Preview("Betting Hint - Positive Count") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.felt.ignoresSafeArea()
|
Color.Table.felt.ignoresSafeArea()
|
||||||
BlackjackBettingHintView(hint: "Bet 4x minimum", trueCount: 2.5)
|
BettingHintView(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()
|
||||||
BlackjackBettingHintView(hint: "Bet minimum", trueCount: -1.5)
|
BettingHintView(hint: "Bet minimum", trueCount: -1.5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Betting Hint - Neutral") {
|
#Preview("Betting Hint - Neutral") {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.Table.felt.ignoresSafeArea()
|
Color.Table.felt.ignoresSafeArea()
|
||||||
BlackjackBettingHintView(hint: "Bet minimum (neutral)", trueCount: 0.0)
|
BettingHintView(hint: "Bet minimum (neutral)", trueCount: 0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,13 +41,8 @@
|
|||||||
// - 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)
|
|
||||||
|
|
||||||
// MARK: - Zones
|
// MARK: - Zones
|
||||||
// - BettingZone
|
// - BettingZone
|
||||||
|
|||||||
@ -334,29 +334,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add $%lld more to meet minimum" : {
|
|
||||||
"comment" : "Hint shown when bet is below table minimum. The argument is the dollar amount needed.",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Add $%lld more to meet minimum"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es-MX" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Agrega $%lld más para alcanzar el mínimo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr-CA" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Ajoutez %lld $ de plus pour atteindre le minimum"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"All game data is stored:" : {
|
"All game data is stored:" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -474,10 +451,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Betting Hint" : {
|
|
||||||
"comment" : "The accessibility label for the betting hint view.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Card face down" : {
|
"Card face down" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -588,33 +561,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Clear" : {
|
|
||||||
"comment" : "A button with an \"x\" icon and \"Clear\" label.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Clear bet" : {
|
|
||||||
"comment" : "Accessibility label for the Clear button that removes all bets.",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Clear bet"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es-MX" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Borrar apuesta"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr-CA" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Effacer la mise"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Clubs" : {
|
"Clubs" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -683,33 +629,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Deal" : {
|
|
||||||
"comment" : "A button that deals cards when pressed.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Deal cards" : {
|
|
||||||
"comment" : "Accessibility label for the Deal button that starts dealing cards.",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Deal cards"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es-MX" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Repartir cartas"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr-CA" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Distribuer les cartes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Dealing Speed" : {
|
"Dealing Speed" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -229,23 +229,6 @@ 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
|
||||||
|
|||||||
@ -1,171 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
//
|
|
||||||
// BettingActionsView.swift
|
|
||||||
// CasinoKit
|
|
||||||
//
|
|
||||||
// Reusable Clear/Deal button pair for casino games betting phase.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// A reusable view that displays Clear and Deal buttons for casino game betting phases.
|
|
||||||
///
|
|
||||||
/// This view follows the "always visible, disabled when unavailable" pattern for better UX:
|
|
||||||
/// - Buttons are always shown to maintain layout stability
|
|
||||||
/// - Clear is disabled when no bet is placed
|
|
||||||
/// - Deal is disabled when the bet doesn't meet requirements
|
|
||||||
/// - Optional minimum bet hint displays when bet is below minimum
|
|
||||||
public struct BettingActionsView: View {
|
|
||||||
|
|
||||||
// MARK: - Properties
|
|
||||||
|
|
||||||
/// Whether the player has any bet placed.
|
|
||||||
public let hasBet: Bool
|
|
||||||
|
|
||||||
/// Whether the deal action is currently available.
|
|
||||||
public let canDeal: Bool
|
|
||||||
|
|
||||||
/// Amount needed to reach minimum bet (shown as hint when > 0).
|
|
||||||
public let amountNeededForMinimum: Int?
|
|
||||||
|
|
||||||
/// Action when Clear is tapped.
|
|
||||||
public let onClear: () -> Void
|
|
||||||
|
|
||||||
/// Action when Deal is tapped.
|
|
||||||
public let onDeal: () -> Void
|
|
||||||
|
|
||||||
// MARK: - Environment
|
|
||||||
|
|
||||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
|
||||||
|
|
||||||
/// Whether the current text size is an accessibility size (very large).
|
|
||||||
private var isAccessibilitySize: Bool {
|
|
||||||
dynamicTypeSize.isAccessibilitySize
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Layout Constants
|
|
||||||
|
|
||||||
private let buttonFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge
|
|
||||||
private let iconSize: CGFloat = CasinoDesign.BaseFontSize.xxLarge + CasinoDesign.Spacing.xSmall
|
|
||||||
private let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
|
|
||||||
/// Creates a betting actions view.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - hasBet: Whether there is any bet placed.
|
|
||||||
/// - canDeal: Whether deal is currently available.
|
|
||||||
/// - amountNeededForMinimum: Optional amount needed to reach minimum (shown as hint).
|
|
||||||
/// - onClear: Action when Clear is tapped.
|
|
||||||
/// - onDeal: Action when Deal is tapped.
|
|
||||||
public init(
|
|
||||||
hasBet: Bool,
|
|
||||||
canDeal: Bool,
|
|
||||||
amountNeededForMinimum: Int? = nil,
|
|
||||||
onClear: @escaping () -> Void,
|
|
||||||
onDeal: @escaping () -> Void
|
|
||||||
) {
|
|
||||||
self.hasBet = hasBet
|
|
||||||
self.canDeal = canDeal
|
|
||||||
self.amountNeededForMinimum = amountNeededForMinimum
|
|
||||||
self.onClear = onClear
|
|
||||||
self.onDeal = onDeal
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Body
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
VStack(spacing: CasinoDesign.Spacing.small) {
|
|
||||||
// Show hint if bet is below minimum
|
|
||||||
if let amount = amountNeededForMinimum, amount > 0 {
|
|
||||||
Text(String(localized: "Add $\(amount) more to meet minimum", bundle: .module))
|
|
||||||
.font(.system(size: hintFontSize, weight: .medium))
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
.transition(.opacity.combined(with: .scale(scale: 0.8)))
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
|
||||||
clearButton
|
|
||||||
dealButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.animation(.easeInOut(duration: CasinoDesign.Animation.standard), value: amountNeededForMinimum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private Views
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var clearButton: some View {
|
|
||||||
let isEnabled = hasBet
|
|
||||||
|
|
||||||
if isAccessibilitySize {
|
|
||||||
Button("Clear", systemImage: "xmark.circle", action: onClear)
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.font(.system(size: iconSize, weight: .semibold))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.padding(CasinoDesign.Spacing.medium + CasinoDesign.Spacing.xxSmall)
|
|
||||||
.background(
|
|
||||||
Circle()
|
|
||||||
.fill(Color.CasinoButton.destructive)
|
|
||||||
)
|
|
||||||
.opacity(isEnabled ? 1.0 : CasinoDesign.Opacity.disabled)
|
|
||||||
.disabled(!isEnabled)
|
|
||||||
.accessibilityLabel(String(localized: "Clear bet", bundle: .module))
|
|
||||||
} else {
|
|
||||||
Button("Clear", systemImage: "xmark.circle", action: onClear)
|
|
||||||
.labelStyle(.titleAndIcon)
|
|
||||||
.font(.system(size: buttonFontSize, weight: .semibold))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.padding(.horizontal, CasinoDesign.Spacing.xxLarge)
|
|
||||||
.padding(.vertical, CasinoDesign.Spacing.medium + CasinoDesign.Spacing.xxSmall)
|
|
||||||
.background(
|
|
||||||
Capsule()
|
|
||||||
.fill(Color.CasinoButton.destructive)
|
|
||||||
)
|
|
||||||
.opacity(isEnabled ? 1.0 : CasinoDesign.Opacity.disabled)
|
|
||||||
.disabled(!isEnabled)
|
|
||||||
.accessibilityLabel(String(localized: "Clear bet", bundle: .module))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var dealButton: some View {
|
|
||||||
let buttonBackground = LinearGradient(
|
|
||||||
colors: [Color.CasinoButton.goldLight, Color.CasinoButton.goldDark],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
if isAccessibilitySize {
|
|
||||||
Button("Deal", systemImage: "play.fill", action: onDeal)
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.font(.system(size: iconSize, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.padding(CasinoDesign.Spacing.medium + CasinoDesign.Spacing.xxSmall)
|
|
||||||
.background(
|
|
||||||
Circle()
|
|
||||||
.fill(buttonBackground)
|
|
||||||
)
|
|
||||||
.shadow(color: .yellow.opacity(CasinoDesign.Opacity.light), radius: CasinoDesign.Shadow.radiusMedium)
|
|
||||||
.opacity(canDeal ? 1.0 : CasinoDesign.Opacity.disabled)
|
|
||||||
.disabled(!canDeal)
|
|
||||||
.accessibilityLabel(String(localized: "Deal cards", bundle: .module))
|
|
||||||
} else {
|
|
||||||
Button("Deal", systemImage: "play.fill", action: onDeal)
|
|
||||||
.labelStyle(.titleAndIcon)
|
|
||||||
.font(.system(size: buttonFontSize, weight: .bold))
|
|
||||||
.foregroundStyle(.black)
|
|
||||||
.padding(.horizontal, CasinoDesign.Spacing.xxLarge)
|
|
||||||
.padding(.vertical, CasinoDesign.Spacing.medium + CasinoDesign.Spacing.xxSmall)
|
|
||||||
.background(
|
|
||||||
Capsule()
|
|
||||||
.fill(buttonBackground)
|
|
||||||
)
|
|
||||||
.shadow(color: .yellow.opacity(CasinoDesign.Opacity.light), radius: CasinoDesign.Shadow.radiusMedium)
|
|
||||||
.opacity(canDeal ? 1.0 : CasinoDesign.Opacity.disabled)
|
|
||||||
.disabled(!canDeal)
|
|
||||||
.accessibilityLabel(String(localized: "Deal cards", bundle: .module))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Previews
|
|
||||||
|
|
||||||
#Preview("No Bet Placed") {
|
|
||||||
ZStack {
|
|
||||||
Color.CasinoTable.felt.ignoresSafeArea()
|
|
||||||
BettingActionsView(
|
|
||||||
hasBet: false,
|
|
||||||
canDeal: false,
|
|
||||||
onClear: {},
|
|
||||||
onDeal: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("Bet Below Minimum") {
|
|
||||||
ZStack {
|
|
||||||
Color.CasinoTable.felt.ignoresSafeArea()
|
|
||||||
BettingActionsView(
|
|
||||||
hasBet: true,
|
|
||||||
canDeal: false,
|
|
||||||
amountNeededForMinimum: 25,
|
|
||||||
onClear: {},
|
|
||||||
onDeal: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("Ready to Deal") {
|
|
||||||
ZStack {
|
|
||||||
Color.CasinoTable.felt.ignoresSafeArea()
|
|
||||||
BettingActionsView(
|
|
||||||
hasBet: true,
|
|
||||||
canDeal: true,
|
|
||||||
onClear: {},
|
|
||||||
onDeal: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user