Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
10d3d02cb0
commit
f1b834c47e
@ -47,7 +47,29 @@ struct BetResult: Identifiable {
|
|||||||
/// Main observable game state class managing all game logic and UI state.
|
/// Main observable game state class managing all game logic and UI state.
|
||||||
@Observable
|
@Observable
|
||||||
@MainActor
|
@MainActor
|
||||||
final class GameState {
|
final class GameState: SessionManagedGame {
|
||||||
|
// MARK: - SessionManagedGame
|
||||||
|
|
||||||
|
typealias Stats = BaccaratStats
|
||||||
|
|
||||||
|
/// The currently active session.
|
||||||
|
var currentSession: BaccaratSession?
|
||||||
|
|
||||||
|
/// History of completed sessions.
|
||||||
|
var sessionHistory: [BaccaratSession] = []
|
||||||
|
|
||||||
|
/// Starting balance for new sessions (from settings).
|
||||||
|
var startingBalance: Int { settings.startingBalance }
|
||||||
|
|
||||||
|
/// Current game style identifier (deck count for Baccarat).
|
||||||
|
var currentGameStyle: String { settings.deckCount.displayName }
|
||||||
|
|
||||||
|
/// Whether a session end has been requested (shows confirmation).
|
||||||
|
var showEndSessionConfirmation: Bool = false
|
||||||
|
|
||||||
|
/// Round histories for completed sessions (keyed by session ID string).
|
||||||
|
private var sessionRoundHistories: [String: [SavedRoundResult]] = [:]
|
||||||
|
|
||||||
// MARK: - Settings
|
// MARK: - Settings
|
||||||
let settings: GameSettings
|
let settings: GameSettings
|
||||||
|
|
||||||
@ -58,13 +80,13 @@ final class GameState {
|
|||||||
private let sound = SoundManager.shared
|
private let sound = SoundManager.shared
|
||||||
|
|
||||||
// MARK: - Persistence
|
// MARK: - Persistence
|
||||||
private var persistence: CloudSyncManager<BaccaratGameData>!
|
let persistence: CloudSyncManager<BaccaratGameData>
|
||||||
|
|
||||||
// MARK: - Game Engine
|
// MARK: - Game Engine
|
||||||
private(set) var engine: BaccaratEngine
|
private(set) var engine: BaccaratEngine
|
||||||
|
|
||||||
// MARK: - Player State
|
// MARK: - Player State
|
||||||
var balance: Int = 10_000
|
var balance: Int = 1_000
|
||||||
var currentBets: [Bet] = []
|
var currentBets: [Bet] = []
|
||||||
|
|
||||||
// MARK: - Round State
|
// MARK: - Round State
|
||||||
@ -135,6 +157,11 @@ final class GameState {
|
|||||||
Array(roundHistory.suffix(20))
|
Array(roundHistory.suffix(20))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the game is over (can't afford to meet minimum bet).
|
||||||
|
var isGameOver: Bool {
|
||||||
|
currentPhase == .betting && balance < settings.minBet
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Hint System
|
// MARK: - Hint System
|
||||||
|
|
||||||
/// The current streak type (player wins, banker wins, or alternating/none).
|
/// The current streak type (player wins, banker wins, or alternating/none).
|
||||||
@ -365,41 +392,39 @@ final class GameState {
|
|||||||
self.engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
|
self.engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
|
||||||
self.balance = settings.startingBalance
|
self.balance = settings.startingBalance
|
||||||
self.onboarding = OnboardingState(gameIdentifier: "baccarat")
|
self.onboarding = OnboardingState(gameIdentifier: "baccarat")
|
||||||
|
self.persistence = CloudSyncManager<BaccaratGameData>()
|
||||||
|
|
||||||
// Sync sound settings with SoundManager
|
// Sync sound settings with SoundManager
|
||||||
syncSoundSettings()
|
syncSoundSettings()
|
||||||
|
|
||||||
// Initialize persistence with cloud data callback
|
// Set up iCloud callback
|
||||||
self.persistence = CloudSyncManager<BaccaratGameData>()
|
|
||||||
persistence.onCloudDataReceived = { [weak self] cloudData in
|
persistence.onCloudDataReceived = { [weak self] cloudData in
|
||||||
self?.handleCloudDataReceived(cloudData)
|
self?.handleCloudDataReceived(cloudData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load saved game data
|
// Load saved game data
|
||||||
loadSavedGame()
|
loadSavedGame()
|
||||||
|
|
||||||
|
// Ensure we have an active session
|
||||||
|
ensureActiveSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles data received from iCloud (e.g., after fresh install or from another device).
|
/// Handles data received from iCloud (e.g., after fresh install or from another device).
|
||||||
private func handleCloudDataReceived(_ cloudData: BaccaratGameData) {
|
private func handleCloudDataReceived(_ cloudData: BaccaratGameData) {
|
||||||
|
|
||||||
// Only update if cloud has more progress than current state
|
// Only update if cloud has more progress than current state
|
||||||
guard cloudData.roundsPlayed > roundHistory.count else {
|
guard cloudData.roundsPlayed > (currentSession?.roundsPlayed ?? 0) + sessionHistory.reduce(0, { $0 + $1.roundsPlayed }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore balance
|
// Restore balance and sessions
|
||||||
self.balance = cloudData.balance
|
self.balance = cloudData.balance
|
||||||
|
self.currentSession = cloudData.currentSession
|
||||||
|
self.sessionHistory = cloudData.sessionHistory
|
||||||
|
self.sessionRoundHistories = cloudData.sessionRoundHistories
|
||||||
|
|
||||||
// Restore round history
|
// Restore round history for road map display
|
||||||
self.roundHistory = cloudData.roundHistory.compactMap { saved in
|
self.roundHistory = cloudData.currentSessionRoundHistory.compactMap { saved in
|
||||||
guard let result = GameResult(persistenceKey: saved.result) else { return nil }
|
saved.toRoundResult()
|
||||||
return RoundResult(
|
|
||||||
result: result,
|
|
||||||
playerValue: saved.playerValue,
|
|
||||||
bankerValue: saved.bankerValue,
|
|
||||||
playerPair: saved.playerPair,
|
|
||||||
bankerPair: saved.bankerPair
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,55 +432,59 @@ final class GameState {
|
|||||||
|
|
||||||
/// Loads saved game data from iCloud/local storage.
|
/// Loads saved game data from iCloud/local storage.
|
||||||
private func loadSavedGame() {
|
private func loadSavedGame() {
|
||||||
let savedData = persistence.data
|
let data = persistence.load()
|
||||||
|
self.balance = data.balance
|
||||||
|
self.currentSession = data.currentSession
|
||||||
|
self.sessionHistory = data.sessionHistory
|
||||||
|
self.sessionRoundHistories = data.sessionRoundHistories
|
||||||
|
|
||||||
// Only restore if there's saved progress
|
// Restore round history for road map display
|
||||||
guard savedData.roundsPlayed > 0 else { return }
|
self.roundHistory = data.currentSessionRoundHistory.compactMap { saved in
|
||||||
|
saved.toRoundResult()
|
||||||
|
}
|
||||||
|
|
||||||
// Restore balance
|
CasinoDesign.debugLog("📂 Loaded game data:")
|
||||||
self.balance = savedData.balance
|
CasinoDesign.debugLog(" - balance: \(data.balance)")
|
||||||
|
CasinoDesign.debugLog(" - currentSession: \(data.currentSession?.id.uuidString ?? "none")")
|
||||||
// Restore round history (convert saved to RoundResult)
|
CasinoDesign.debugLog(" - sessionHistory count: \(data.sessionHistory.count)")
|
||||||
self.roundHistory = savedData.roundHistory.compactMap { saved in
|
CasinoDesign.debugLog(" - roundHistory count: \(roundHistory.count)")
|
||||||
guard let result = GameResult(persistenceKey: saved.result) else { return nil }
|
CasinoDesign.debugLog(" - sessionRoundHistories count: \(sessionRoundHistories.count)")
|
||||||
return RoundResult(
|
if let session = data.currentSession {
|
||||||
result: result,
|
CasinoDesign.debugLog(" - current session rounds: \(session.roundsPlayed), duration: \(session.duration)s")
|
||||||
playerValue: saved.playerValue,
|
|
||||||
bankerValue: saved.bankerValue,
|
|
||||||
playerPair: saved.playerPair,
|
|
||||||
bankerPair: saved.bankerPair
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves current game state to iCloud/local storage.
|
/// Saves current game state to iCloud/local storage.
|
||||||
private func saveGame(netWinnings: Int = 0) {
|
func saveGameData() {
|
||||||
var data = persistence.data
|
// Update current session before saving
|
||||||
|
if var session = currentSession {
|
||||||
|
session.endingBalance = balance
|
||||||
|
// Keep session's game style in sync with current settings
|
||||||
|
session.gameStyle = currentGameStyle
|
||||||
|
currentSession = session
|
||||||
|
|
||||||
// Update balance
|
// Always keep the current session's round history in the archive
|
||||||
data.balance = balance
|
// This ensures it's available when the session ends and becomes historical
|
||||||
|
if !roundHistory.isEmpty {
|
||||||
// Update statistics
|
let savedHistory = roundHistory.map { SavedRoundResult(from: $0) }
|
||||||
data.totalWinnings += netWinnings
|
sessionRoundHistories[session.id.uuidString] = savedHistory
|
||||||
if netWinnings > data.biggestWin {
|
|
||||||
data.biggestWin = netWinnings
|
|
||||||
}
|
}
|
||||||
if netWinnings < 0 && abs(netWinnings) > data.biggestLoss {
|
|
||||||
data.biggestLoss = abs(netWinnings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update round history from current session
|
// Convert round history for persistence
|
||||||
data.roundHistory = roundHistory.enumerated().map { index, round in
|
let savedRoundHistory = roundHistory.map { SavedRoundResult(from: $0) }
|
||||||
// Try to get existing saved result for net winnings
|
|
||||||
if index < data.roundHistory.count {
|
|
||||||
return data.roundHistory[index]
|
|
||||||
}
|
|
||||||
// New round - calculate net winnings from betResults if available
|
|
||||||
let netForRound = betResults.reduce(0) { $0 + $1.payout }
|
|
||||||
return SavedRoundResult(from: round, netWinnings: index == roundHistory.count - 1 ? netWinnings : netForRound)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let data = BaccaratGameData(
|
||||||
|
lastModified: Date(),
|
||||||
|
balance: balance,
|
||||||
|
currentSession: currentSession,
|
||||||
|
sessionHistory: sessionHistory,
|
||||||
|
currentSessionRoundHistory: savedRoundHistory,
|
||||||
|
sessionRoundHistories: sessionRoundHistories
|
||||||
|
)
|
||||||
persistence.save(data)
|
persistence.save(data)
|
||||||
|
|
||||||
|
CasinoDesign.debugLog("💾 Saved game data - session rounds: \(currentSession?.roundsPlayed ?? 0), road history: \(roundHistory.count)")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether iCloud sync is available.
|
/// Whether iCloud sync is available.
|
||||||
@ -748,10 +777,17 @@ final class GameState {
|
|||||||
playerHadPair = engine.playerHasPair
|
playerHadPair = engine.playerHasPair
|
||||||
bankerHadPair = engine.bankerHasPair
|
bankerHadPair = engine.bankerHasPair
|
||||||
|
|
||||||
|
// Check for naturals
|
||||||
|
let isNatural = engine.playerHand.isNatural || engine.bankerHand.isNatural
|
||||||
|
|
||||||
// Calculate and apply payouts, track individual results
|
// Calculate and apply payouts, track individual results
|
||||||
var totalWinnings = 0
|
var totalWinnings = 0
|
||||||
var results: [BetResult] = []
|
var results: [BetResult] = []
|
||||||
|
|
||||||
|
// Track dragon bonus wins
|
||||||
|
var dragonPlayerWon = false
|
||||||
|
var dragonBankerWon = false
|
||||||
|
|
||||||
for bet in currentBets {
|
for bet in currentBets {
|
||||||
let payout = engine.calculatePayout(bet: bet, result: result)
|
let payout = engine.calculatePayout(bet: bet, result: result)
|
||||||
totalWinnings += payout
|
totalWinnings += payout
|
||||||
@ -759,6 +795,14 @@ final class GameState {
|
|||||||
// Track individual bet result
|
// Track individual bet result
|
||||||
results.append(BetResult(type: bet.type, amount: bet.amount, payout: payout))
|
results.append(BetResult(type: bet.type, amount: bet.amount, payout: payout))
|
||||||
|
|
||||||
|
// Track dragon bonus wins
|
||||||
|
if bet.type == .dragonBonusPlayer && payout > 0 {
|
||||||
|
dragonPlayerWon = true
|
||||||
|
}
|
||||||
|
if bet.type == .dragonBonusBanker && payout > 0 {
|
||||||
|
dragonBankerWon = true
|
||||||
|
}
|
||||||
|
|
||||||
// Return original bet if not a loss
|
// Return original bet if not a loss
|
||||||
if payout >= 0 {
|
if payout >= 0 {
|
||||||
balance += bet.amount
|
balance += bet.amount
|
||||||
@ -773,6 +817,40 @@ final class GameState {
|
|||||||
betResults = results
|
betResults = results
|
||||||
lastWinnings = totalWinnings
|
lastWinnings = totalWinnings
|
||||||
|
|
||||||
|
// Determine round outcome for session stats
|
||||||
|
let isWin = totalWinnings > 0
|
||||||
|
let isLoss = totalWinnings < 0
|
||||||
|
let outcome: RoundOutcome = isWin ? .win : (isLoss ? .lose : .push)
|
||||||
|
|
||||||
|
// Capture values for closure
|
||||||
|
let roundBetAmount = totalBetAmount
|
||||||
|
let wasPlayerWin = result == .playerWins
|
||||||
|
let wasBankerWin = result == .bankerWins
|
||||||
|
let wasTie = result == .tie
|
||||||
|
let hadPlayerPair = playerHadPair
|
||||||
|
let hadBankerPair = bankerHadPair
|
||||||
|
|
||||||
|
// Record round in session using CasinoKit protocol
|
||||||
|
recordSessionRound(
|
||||||
|
winnings: totalWinnings,
|
||||||
|
betAmount: roundBetAmount,
|
||||||
|
outcome: outcome
|
||||||
|
) { stats in
|
||||||
|
// Update Baccarat-specific stats
|
||||||
|
if isNatural { stats.naturals += 1 }
|
||||||
|
if wasPlayerWin { stats.playerWins += 1 }
|
||||||
|
if wasBankerWin { stats.bankerWins += 1 }
|
||||||
|
if wasTie { stats.ties += 1 }
|
||||||
|
if hadPlayerPair { stats.playerPairs += 1 }
|
||||||
|
if hadBankerPair { stats.bankerPairs += 1 }
|
||||||
|
if dragonPlayerWon { stats.dragonBonusPlayerWins += 1 }
|
||||||
|
if dragonBankerWon { stats.dragonBonusBankerWins += 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
CasinoDesign.debugLog("📊 Session stats update:")
|
||||||
|
CasinoDesign.debugLog(" - roundsPlayed: \(currentSession?.roundsPlayed ?? 0)")
|
||||||
|
CasinoDesign.debugLog(" - duration: \(currentSession?.duration ?? 0) seconds")
|
||||||
|
|
||||||
// Play result sound based on MAIN BET outcome (not total winnings)
|
// Play result sound based on MAIN BET outcome (not total winnings)
|
||||||
// This way winning the main hand plays win sound even if side bets lost
|
// This way winning the main hand plays win sound even if side bets lost
|
||||||
let mainBetResult = results.first(where: { $0.type == .player || $0.type == .banker })
|
let mainBetResult = results.first(where: { $0.type == .player || $0.type == .banker })
|
||||||
@ -801,7 +879,7 @@ final class GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record result in history
|
// Record result in history (for road map)
|
||||||
roundHistory.append(RoundResult(
|
roundHistory.append(RoundResult(
|
||||||
result: result,
|
result: result,
|
||||||
playerValue: playerHandValue,
|
playerValue: playerHandValue,
|
||||||
@ -810,8 +888,8 @@ final class GameState {
|
|||||||
bankerPair: bankerHadPair
|
bankerPair: bankerHadPair
|
||||||
))
|
))
|
||||||
|
|
||||||
// Save game state to iCloud/local
|
// Save game data to iCloud
|
||||||
saveGame(netWinnings: totalWinnings)
|
saveGameData()
|
||||||
|
|
||||||
// Show result banner - stays until user taps New Round
|
// Show result banner - stays until user taps New Round
|
||||||
showResultBanner = true
|
showResultBanner = true
|
||||||
@ -851,41 +929,98 @@ final class GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the game to initial state with current settings.
|
// MARK: - Session History Management
|
||||||
func resetGame() {
|
|
||||||
|
/// Deletes a session from history by ID.
|
||||||
|
func deleteSession(id: UUID) {
|
||||||
|
sessionHistory.removeAll { $0.id == id }
|
||||||
|
sessionRoundHistories.removeValue(forKey: id.uuidString)
|
||||||
|
saveGameData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes all session history.
|
||||||
|
func deleteAllSessionHistory() {
|
||||||
|
sessionHistory.removeAll()
|
||||||
|
sessionRoundHistories.removeAll()
|
||||||
|
saveGameData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Session Round History
|
||||||
|
|
||||||
|
/// Gets round history for a specific session.
|
||||||
|
func roundHistory(for session: BaccaratSession) -> [RoundResult] {
|
||||||
|
// If it's the current session, return the live round history
|
||||||
|
if session.id == currentSession?.id {
|
||||||
|
return roundHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise look up from archived histories
|
||||||
|
guard let savedHistory = sessionRoundHistories[session.id.uuidString] else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedHistory.compactMap { $0.toRoundResult() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SessionManagedGame Implementation
|
||||||
|
|
||||||
|
/// Resets game-specific state when starting a new session.
|
||||||
|
func resetForNewSession() {
|
||||||
|
// Note: Round history is already archived during saveGameData() calls
|
||||||
|
// Just clear the local round history for the new session
|
||||||
|
roundHistory = []
|
||||||
engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
|
engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
|
||||||
balance = settings.startingBalance
|
newRoundInternal()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal new round reset (without sound).
|
||||||
|
private func newRoundInternal() {
|
||||||
currentBets = []
|
currentBets = []
|
||||||
currentPhase = .betting
|
|
||||||
lastResult = nil
|
|
||||||
lastWinnings = 0
|
|
||||||
visiblePlayerCards = []
|
visiblePlayerCards = []
|
||||||
visibleBankerCards = []
|
visibleBankerCards = []
|
||||||
playerCardsFaceUp = []
|
playerCardsFaceUp = []
|
||||||
bankerCardsFaceUp = []
|
bankerCardsFaceUp = []
|
||||||
roundHistory = []
|
lastResult = nil
|
||||||
isAnimating = false
|
lastWinnings = 0
|
||||||
showResultBanner = false
|
|
||||||
playerHadPair = false
|
playerHadPair = false
|
||||||
bankerHadPair = false
|
bankerHadPair = false
|
||||||
betResults = []
|
betResults = []
|
||||||
|
currentPhase = .betting
|
||||||
|
}
|
||||||
|
|
||||||
// Save the reset state (keeps lifetime stats, resets balance and session history)
|
/// Aggregated Baccarat-specific stats from all sessions.
|
||||||
saveGame()
|
var aggregatedBaccaratStats: BaccaratStats {
|
||||||
|
allSessions.aggregatedBaccaratStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Game Reset
|
||||||
|
|
||||||
|
/// Resets the entire game (keeps statistics).
|
||||||
|
func resetGame() {
|
||||||
|
balance = settings.startingBalance
|
||||||
|
roundHistory = []
|
||||||
|
engine = BaccaratEngine(deckCount: settings.deckCount.rawValue)
|
||||||
|
startNewSession()
|
||||||
|
newRoundInternal()
|
||||||
|
saveGameData()
|
||||||
|
|
||||||
// Play new game sound
|
// Play new game sound
|
||||||
sound.playNewRound()
|
sound.playNewRound()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Completely clears all saved data and starts fresh (including lifetime stats).
|
/// Completely clears all saved data and starts fresh.
|
||||||
func clearAllData() {
|
func clearAllData() {
|
||||||
persistence.reset()
|
persistence.reset()
|
||||||
resetGame()
|
balance = settings.startingBalance
|
||||||
}
|
currentSession = nil
|
||||||
|
sessionHistory = []
|
||||||
|
sessionRoundHistories = [:]
|
||||||
|
roundHistory = []
|
||||||
|
startNewSession()
|
||||||
|
newRoundInternal()
|
||||||
|
|
||||||
/// Returns lifetime statistics from saved data.
|
// Play new game sound
|
||||||
var lifetimeStats: BaccaratGameData {
|
sound.playNewRound()
|
||||||
persistence.data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies new settings (call after settings change).
|
/// Applies new settings (call after settings change).
|
||||||
|
|||||||
111
Baccarat/Baccarat/Models/BaccaratStats.swift
Normal file
111
Baccarat/Baccarat/Models/BaccaratStats.swift
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
//
|
||||||
|
// BaccaratStats.swift
|
||||||
|
// Baccarat
|
||||||
|
//
|
||||||
|
// Baccarat-specific statistics that conform to CasinoKit's GameSpecificStats.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import CasinoKit
|
||||||
|
|
||||||
|
/// Baccarat-specific session statistics.
|
||||||
|
/// Tracks naturals, banker/player wins, ties, pairs, and dragon bonus wins.
|
||||||
|
public struct BaccaratStats: GameSpecificStats {
|
||||||
|
/// Number of natural hands (8 or 9 on initial deal).
|
||||||
|
public var naturals: Int = 0
|
||||||
|
|
||||||
|
/// Number of banker win rounds.
|
||||||
|
public var bankerWins: Int = 0
|
||||||
|
|
||||||
|
/// Number of player win rounds.
|
||||||
|
public var playerWins: Int = 0
|
||||||
|
|
||||||
|
/// Number of tie rounds.
|
||||||
|
public var ties: Int = 0
|
||||||
|
|
||||||
|
/// Number of player pair occurrences.
|
||||||
|
public var playerPairs: Int = 0
|
||||||
|
|
||||||
|
/// Number of banker pair occurrences.
|
||||||
|
public var bankerPairs: Int = 0
|
||||||
|
|
||||||
|
/// Number of dragon bonus wins (player side).
|
||||||
|
public var dragonBonusPlayerWins: Int = 0
|
||||||
|
|
||||||
|
/// Number of dragon bonus wins (banker side).
|
||||||
|
public var dragonBonusBankerWins: Int = 0
|
||||||
|
|
||||||
|
// MARK: - GameSpecificStats
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
/// Display items for the statistics UI.
|
||||||
|
public var displayItems: [StatDisplayItem] {
|
||||||
|
[
|
||||||
|
StatDisplayItem(
|
||||||
|
icon: "sparkles",
|
||||||
|
iconColor: .yellow,
|
||||||
|
label: String(localized: "Naturals"),
|
||||||
|
value: "\(naturals)",
|
||||||
|
valueColor: .yellow
|
||||||
|
),
|
||||||
|
StatDisplayItem(
|
||||||
|
icon: "person.fill",
|
||||||
|
iconColor: .blue,
|
||||||
|
label: String(localized: "Player Wins"),
|
||||||
|
value: "\(playerWins)",
|
||||||
|
valueColor: .blue
|
||||||
|
),
|
||||||
|
StatDisplayItem(
|
||||||
|
icon: "building.columns.fill",
|
||||||
|
iconColor: .red,
|
||||||
|
label: String(localized: "Banker Wins"),
|
||||||
|
value: "\(bankerWins)",
|
||||||
|
valueColor: .red
|
||||||
|
),
|
||||||
|
StatDisplayItem(
|
||||||
|
icon: "equal.circle.fill",
|
||||||
|
iconColor: .green,
|
||||||
|
label: String(localized: "Ties"),
|
||||||
|
value: "\(ties)",
|
||||||
|
valueColor: .green
|
||||||
|
),
|
||||||
|
StatDisplayItem(
|
||||||
|
icon: "suit.diamond.fill",
|
||||||
|
iconColor: .purple,
|
||||||
|
label: String(localized: "Pairs"),
|
||||||
|
value: "\(playerPairs + bankerPairs)",
|
||||||
|
valueColor: .purple
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Aggregation Extension
|
||||||
|
|
||||||
|
extension Array where Element == GameSession<BaccaratStats> {
|
||||||
|
/// Aggregates Baccarat-specific stats from all sessions.
|
||||||
|
func aggregatedBaccaratStats() -> BaccaratStats {
|
||||||
|
var combined = BaccaratStats()
|
||||||
|
|
||||||
|
for session in self {
|
||||||
|
combined.naturals += session.gameStats.naturals
|
||||||
|
combined.bankerWins += session.gameStats.bankerWins
|
||||||
|
combined.playerWins += session.gameStats.playerWins
|
||||||
|
combined.ties += session.gameStats.ties
|
||||||
|
combined.playerPairs += session.gameStats.playerPairs
|
||||||
|
combined.bankerPairs += session.gameStats.bankerPairs
|
||||||
|
combined.dragonBonusPlayerWins += session.gameStats.dragonBonusPlayerWins
|
||||||
|
combined.dragonBonusBankerWins += session.gameStats.dragonBonusBankerWins
|
||||||
|
}
|
||||||
|
|
||||||
|
return combined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Type Aliases for Convenience
|
||||||
|
|
||||||
|
/// Baccarat session type alias.
|
||||||
|
public typealias BaccaratSession = GameSession<BaccaratStats>
|
||||||
|
|
||||||
@ -354,6 +354,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"$%lld" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"2-9: Face value" : {
|
"2-9: Face value" : {
|
||||||
"comment" : "Description of the card values for cards with values from 2 to 9.",
|
"comment" : "Description of the card values for cards with values from 2 to 9.",
|
||||||
@ -562,6 +565,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ALL TIME SUMMARY" : {
|
||||||
|
"comment" : "Title of a section in the statistics sheet that provides a summary of the user's performance over all time.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Alternative: Use an online tool" : {
|
"Alternative: Use an online tool" : {
|
||||||
"comment" : "A section header that suggests using an online tool to generate app icon sizes.",
|
"comment" : "A section header that suggests using an online tool to generate app icon sizes.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -677,6 +684,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Average bet" : {
|
||||||
|
"comment" : "The value of this row is calculated as the total bet amount divided by the number of rounds played.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Avoid the Tie bet — 14.4% house edge!" : {
|
"Avoid the Tie bet — 14.4% house edge!" : {
|
||||||
"comment" : "Tip for avoiding the Tie bet in baccarat, highlighting its low house edge.",
|
"comment" : "Tip for avoiding the Tie bet in baccarat, highlighting its low house edge.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -701,6 +712,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"B Pair" : {
|
"B Pair" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -747,6 +759,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"BALANCE" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Banker" : {
|
"Banker" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1012,6 +1027,18 @@
|
|||||||
"comment" : "A hint to place a bet on the Banker based on the calculated house edge. The argument is the percentage of the house edge.",
|
"comment" : "A hint to place a bet on the Banker based on the calculated house edge. The argument is the percentage of the house edge.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Banker Wins" : {
|
||||||
|
"comment" : "Label for the number of banker win rounds in the statistics display.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Best gain" : {
|
||||||
|
"comment" : "\"Best gain\" is a colloquial term for the largest positive winnings in a single game.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Best session" : {
|
||||||
|
"comment" : "A label for the best session amount in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Bet on Player, Banker, or Tie" : {
|
"Bet on Player, Banker, or Tie" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -1040,6 +1067,14 @@
|
|||||||
},
|
},
|
||||||
"Betting tips and trend analysis" : {
|
"Betting tips and trend analysis" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"BIG ROAD" : {
|
||||||
|
"comment" : "Title for the section in the statistics sheet that shows the user's performance on the Big Road.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Biggest bet" : {
|
||||||
|
"comment" : "The label for the \"Biggest bet\" row in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Blackjack" : {
|
"Blackjack" : {
|
||||||
"comment" : "The name of a blackjack game.",
|
"comment" : "The name of a blackjack game.",
|
||||||
@ -1227,6 +1262,10 @@
|
|||||||
},
|
},
|
||||||
"Change table limits and display options" : {
|
"Change table limits and display options" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"CHIPS STATS" : {
|
||||||
|
"comment" : "Section that shows statistics related to the user's chip count during a Baccarat session.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Chips, cards, and result sounds" : {
|
"Chips, cards, and result sounds" : {
|
||||||
"comment" : "Subtitle describing sound effects toggle.",
|
"comment" : "Subtitle describing sound effects toggle.",
|
||||||
@ -1343,6 +1382,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Completed sessions will appear here." : {
|
||||||
|
"comment" : "A description below the label indicating that completed sessions will be displayed here.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Current" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Customize Settings" : {
|
"Customize Settings" : {
|
||||||
|
|
||||||
@ -1439,6 +1485,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Delete" : {
|
||||||
|
"comment" : "A button to delete a session.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Delete Session" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Delete Session?" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"DISPLAY" : {
|
"DISPLAY" : {
|
||||||
"comment" : "Section header for display settings.",
|
"comment" : "Section header for display settings.",
|
||||||
@ -1580,6 +1636,21 @@
|
|||||||
"Dragon Bonus: high risk, high reward" : {
|
"Dragon Bonus: high risk, high reward" : {
|
||||||
"comment" : "Warning text for dragon bonus bets, advising players to be cautious.",
|
"comment" : "Warning text for dragon bonus bets, advising players to be cautious.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"End Session" : {
|
||||||
|
"comment" : "The text for a button that ends the current game session.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"End Session?" : {
|
||||||
|
"comment" : "A confirmation dialog title.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Ended manually" : {
|
||||||
|
"comment" : "A description of a session that ended manually, rather than automatically.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Ending balance" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Example: 5♥ + 5♣ = Pair (wins!)" : {
|
"Example: 5♥ + 5♣ = Pair (wins!)" : {
|
||||||
"comment" : "Example of a pair bet winning.",
|
"comment" : "Example of a pair bet winning.",
|
||||||
@ -1675,6 +1746,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GAME STATS" : {
|
||||||
|
"comment" : "Section in the statistics sheet dedicated to displaying statistics specific to baccarat.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Generate & Save Icons" : {
|
"Generate & Save Icons" : {
|
||||||
"comment" : "A button label that triggers the generation of app icons.",
|
"comment" : "A button label that triggers the generation of app icons.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1744,6 +1819,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Global" : {
|
||||||
|
"comment" : "Title of the statistics tab that shows global statistics.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Green Circle (T): Tie between Player and Banker" : {
|
"Green Circle (T): Tie between Player and Banker" : {
|
||||||
"comment" : "Explains the green circle icon in the history.",
|
"comment" : "Explains the green circle icon in the history.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1790,6 +1869,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Hands" : {
|
||||||
|
"comment" : "Label for the number of hands played in a summary stat column.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"handValueFormat" : {
|
"handValueFormat" : {
|
||||||
"comment" : "Format for displaying hand value. The argument is the numeric value of the hand.",
|
"comment" : "Format for displaying hand value. The argument is the numeric value of the hand.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1836,6 +1919,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"History" : {
|
||||||
|
"comment" : "Title of the statistics tab that shows the user's session history.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"HISTORY" : {
|
"HISTORY" : {
|
||||||
"comment" : "A label displayed above the road map view, indicating that it shows a history of past game results.",
|
"comment" : "A label displayed above the road map view, indicating that it shows a history of past game results.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2245,6 +2332,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Losing sessions" : {
|
||||||
|
"comment" : "A label describing the number of sessions the user has lost.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Lost" : {
|
||||||
|
"comment" : "Labels for the outcome circles in the \"Win/Loss/Push\" section.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Main Bets" : {
|
"Main Bets" : {
|
||||||
"comment" : "Title of a rule page in the \"Rules\" help view, describing the main bets available in baccarat.",
|
"comment" : "Title of a rule page in the \"Rules\" help view, describing the main bets available in baccarat.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2381,6 +2476,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Net" : {
|
||||||
|
"comment" : "Label for the net winnings in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Net result" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Never" : {
|
"Never" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2427,6 +2529,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"No Active Session" : {
|
||||||
|
"comment" : "A message displayed when there is no active session to display in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"No cards" : {
|
"No cards" : {
|
||||||
"comment" : "A description of the player's hand when they have no cards.",
|
"comment" : "A description of the player's hand when they have no cards.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2495,6 +2601,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"No Session History" : {
|
||||||
|
"comment" : "A description displayed when a user has no session history.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Objective" : {
|
"Objective" : {
|
||||||
"comment" : "Title of a rule page in the \"Rules\" help view, describing the objective of the game.",
|
"comment" : "Title of a rule page in the \"Rules\" help view, describing the objective of the game.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2634,6 +2744,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"P Pair" : {
|
"P Pair" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -2732,6 +2843,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Pairs" : {
|
||||||
|
"comment" : "Label for the total number of player and banker pair occurrences in the statistics UI.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Pairs occur roughly once every 15 hands." : {
|
"Pairs occur roughly once every 15 hands." : {
|
||||||
"comment" : "Explanation of how often pairs occur in a typical game of baccarat.",
|
"comment" : "Explanation of how often pairs occur in a typical game of baccarat.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2886,6 +3001,10 @@
|
|||||||
"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.",
|
"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
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Player Wins" : {
|
||||||
|
"comment" : "Label for the \"Player Wins\" stat in the statistics UI.",
|
||||||
|
"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" : {
|
||||||
@ -3003,6 +3122,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Push" : {
|
||||||
|
"comment" : "A label for the \"Push\" outcome in the game stats section.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Ran out of chips" : {
|
||||||
|
"comment" : "A description of why a session might have ended with a \"Ran out of chips\" result.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Red Circle (B): Banker won the hand" : {
|
"Red Circle (B): Banker won the hand" : {
|
||||||
"comment" : "Explains the red circle icon in the history.",
|
"comment" : "Explains the red circle icon in the history.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -3129,6 +3256,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Rounds" : {
|
"Rounds" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3150,6 +3278,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Rounds played" : {
|
||||||
|
"comment" : "A label displayed next to the number of rounds played in a session.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Rounds Played" : {
|
"Rounds Played" : {
|
||||||
"comment" : "A label displayed next to the number of rounds played in the game over screen.",
|
"comment" : "A label displayed next to the number of rounds played in the game over screen.",
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@ -3176,6 +3308,14 @@
|
|||||||
},
|
},
|
||||||
"Select a chip and tap a bet zone" : {
|
"Select a chip and tap a bet zone" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"SESSION PERFORMANCE" : {
|
||||||
|
"comment" : "Title of a section in the statistics sheet that details session performance metrics.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Sessions" : {
|
||||||
|
"comment" : "Label for the number of sessions played in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Set a budget and stick to it." : {
|
"Set a budget and stick to it." : {
|
||||||
"comment" : "Tip for players to set a budget and stick to it when playing baccarat.",
|
"comment" : "Tip for players to set a budget and stick to it when playing baccarat.",
|
||||||
@ -3389,8 +3529,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Start playing to begin tracking your session." : {
|
||||||
|
"comment" : "A description below the \"No Active Session\" label, instructing the user to start playing to view their session statistics.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Start with $1,000 and play risk-free" : {
|
"Start with $1,000 and play risk-free" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Starting balance" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"STARTING BALANCE" : {
|
"STARTING BALANCE" : {
|
||||||
"comment" : "Section header for starting balance settings.",
|
"comment" : "Section header for starting balance settings.",
|
||||||
@ -3724,6 +3871,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"This will permanently remove this session from your history." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Tie" : {
|
"Tie" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -3797,6 +3947,14 @@
|
|||||||
"comment" : "Warning message for a tie bet, explaining the high house edge.",
|
"comment" : "Warning message for a tie bet, explaining the high house edge.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Ties" : {
|
||||||
|
"comment" : "Description of a baccarat statistics category for tie rounds.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Time" : {
|
||||||
|
"comment" : "Label for the duration of a session in the statistics sheet.",
|
||||||
|
"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",
|
||||||
@ -3821,6 +3979,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Total bet" : {
|
||||||
|
"comment" : "The value string for the \"Total bet\" row in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Total gain" : {
|
||||||
|
"comment" : "Label for the total gain in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Total game time" : {
|
||||||
|
"comment" : "Rows in the \"Game stats\" section of the statistics sheet, showing various statistics about a Baccarat session.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Total Winnings" : {
|
"Total Winnings" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -4099,6 +4269,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"win rate" : {
|
||||||
|
"comment" : "A label describing the win rate of a session.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Win Rate" : {
|
||||||
|
"comment" : "Label for the win rate in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Winner" : {
|
"Winner" : {
|
||||||
"comment" : "A description of the player's hand, including its value and whether they won.",
|
"comment" : "A description of the player's hand, including its value and whether they won.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -4122,6 +4300,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Winning sessions" : {
|
||||||
|
"comment" : "A title describing the number of sessions the user has won.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Won" : {
|
||||||
|
"comment" : "Labels for the outcome circles in the \"Win/Loss/Push\" section.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Worst loss" : {
|
||||||
|
"comment" : "The label and value for the \"Worst loss\" row are identical to those for the \"Best gain\" row. This is intentional, as it highlights the symmetry in the data.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Worst session" : {
|
||||||
|
"comment" : "A label for the worst session amount in the statistics sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Yellow Dot (bottom-left): A pair occurred in that hand" : {
|
"Yellow Dot (bottom-left): A pair occurred in that hand" : {
|
||||||
"comment" : "Explains the yellow dot marker in the history.",
|
"comment" : "Explains the yellow dot marker in the history.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -4168,6 +4362,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"You played %lld hands with a net result of %@. This session will be saved to your history." : {
|
||||||
|
"comment" : "A message displayed when a user ends a game session. The first argument is the number of rounds played in the session. The second argument is the net result of the session, formatted as currency.",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "You played %1$lld hands with a net result of %2$@. This session will be saved to your history."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"You've run out of chips!" : {
|
"You've run out of chips!" : {
|
||||||
"comment" : "A message displayed when a player runs out of money in the game over screen.",
|
"comment" : "A message displayed when a player runs out of money in the game over screen.",
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
|
|||||||
@ -9,75 +9,54 @@ import Foundation
|
|||||||
import CasinoKit
|
import CasinoKit
|
||||||
|
|
||||||
/// Persisted data for Baccarat game.
|
/// Persisted data for Baccarat game.
|
||||||
public struct BaccaratGameData: PersistableGameData {
|
public struct BaccaratGameData: PersistableGameData, SessionPersistable {
|
||||||
|
|
||||||
// MARK: - PersistableGameData
|
// MARK: - PersistableGameData
|
||||||
|
|
||||||
public static let gameIdentifier = "baccarat"
|
public static let gameIdentifier = "baccarat"
|
||||||
|
|
||||||
public var roundsPlayed: Int {
|
public var roundsPlayed: Int {
|
||||||
roundHistory.count
|
// Total rounds from all sessions
|
||||||
|
let historicalRounds = sessionHistory.reduce(0) { $0 + $1.roundsPlayed }
|
||||||
|
let currentRounds = currentSession?.roundsPlayed ?? 0
|
||||||
|
return historicalRounds + currentRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var empty: BaccaratGameData {
|
public static var empty: BaccaratGameData {
|
||||||
BaccaratGameData(
|
BaccaratGameData(
|
||||||
balance: 10_000,
|
lastModified: Date(),
|
||||||
roundHistory: [],
|
balance: 1_000,
|
||||||
totalWinnings: 0,
|
currentSession: nil,
|
||||||
biggestWin: 0,
|
sessionHistory: [],
|
||||||
biggestLoss: 0,
|
currentSessionRoundHistory: [],
|
||||||
lastModified: Date()
|
sessionRoundHistories: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Game Data
|
// MARK: - Game Data
|
||||||
|
|
||||||
/// Current chip balance.
|
|
||||||
public var balance: Int
|
|
||||||
|
|
||||||
/// History of all rounds played.
|
|
||||||
public var roundHistory: [SavedRoundResult]
|
|
||||||
|
|
||||||
// MARK: - Lifetime Statistics
|
|
||||||
|
|
||||||
/// Total net winnings (can be negative).
|
|
||||||
public var totalWinnings: Int
|
|
||||||
|
|
||||||
/// Biggest single-round win.
|
|
||||||
public var biggestWin: Int
|
|
||||||
|
|
||||||
/// Biggest single-round loss (stored as positive number).
|
|
||||||
public var biggestLoss: Int
|
|
||||||
|
|
||||||
/// Last time data was modified (required by PersistableGameData).
|
/// Last time data was modified (required by PersistableGameData).
|
||||||
public var lastModified: Date
|
public var lastModified: Date
|
||||||
|
|
||||||
// MARK: - Computed Stats
|
/// Current chip balance.
|
||||||
|
public var balance: Int
|
||||||
|
|
||||||
/// Number of Player wins.
|
/// The currently active session (nil if no session started).
|
||||||
public var playerWins: Int {
|
public var currentSession: BaccaratSession?
|
||||||
roundHistory.filter { $0.result == "player" }.count
|
|
||||||
|
/// History of completed sessions.
|
||||||
|
public var sessionHistory: [BaccaratSession]
|
||||||
|
|
||||||
|
/// Round history for the current session (for road map display).
|
||||||
|
/// This is cleared when a new session starts.
|
||||||
|
public var currentSessionRoundHistory: [SavedRoundResult]
|
||||||
|
|
||||||
|
/// Round histories for completed sessions (keyed by session ID string).
|
||||||
|
/// Used to display road maps when viewing historical sessions.
|
||||||
|
public var sessionRoundHistories: [String: [SavedRoundResult]]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Number of Banker wins.
|
/// Codable round result for persistence (for road map display).
|
||||||
public var bankerWins: Int {
|
|
||||||
roundHistory.filter { $0.result == "banker" }.count
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Number of Tie games.
|
|
||||||
public var tieGames: Int {
|
|
||||||
roundHistory.filter { $0.result == "tie" }.count
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Win rate percentage.
|
|
||||||
public var winRate: Double {
|
|
||||||
guard roundsPlayed > 0 else { return 0 }
|
|
||||||
let wins = roundHistory.filter { $0.netWinnings > 0 }.count
|
|
||||||
return Double(wins) / Double(roundsPlayed) * 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Codable round result for persistence.
|
|
||||||
public struct SavedRoundResult: Codable, Identifiable, Sendable {
|
public struct SavedRoundResult: Codable, Identifiable, Sendable {
|
||||||
public let id: UUID
|
public let id: UUID
|
||||||
public let result: String // "player", "banker", "tie"
|
public let result: String // "player", "banker", "tie"
|
||||||
@ -87,7 +66,6 @@ public struct SavedRoundResult: Codable, Identifiable, Sendable {
|
|||||||
public let bankerPair: Bool
|
public let bankerPair: Bool
|
||||||
public let isNatural: Bool
|
public let isNatural: Bool
|
||||||
public let timestamp: Date
|
public let timestamp: Date
|
||||||
public let netWinnings: Int
|
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: UUID = UUID(),
|
id: UUID = UUID(),
|
||||||
@ -97,8 +75,7 @@ public struct SavedRoundResult: Codable, Identifiable, Sendable {
|
|||||||
playerPair: Bool,
|
playerPair: Bool,
|
||||||
bankerPair: Bool,
|
bankerPair: Bool,
|
||||||
isNatural: Bool,
|
isNatural: Bool,
|
||||||
timestamp: Date = Date(),
|
timestamp: Date = Date()
|
||||||
netWinnings: Int
|
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.result = result
|
self.result = result
|
||||||
@ -108,15 +85,14 @@ public struct SavedRoundResult: Codable, Identifiable, Sendable {
|
|||||||
self.bankerPair = bankerPair
|
self.bankerPair = bankerPair
|
||||||
self.isNatural = isNatural
|
self.isNatural = isNatural
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.netWinnings = netWinnings
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Conversion from RoundResult
|
// MARK: - Conversion from RoundResult
|
||||||
|
|
||||||
extension SavedRoundResult {
|
extension SavedRoundResult {
|
||||||
/// Creates a SavedRoundResult from a RoundResult and net winnings.
|
/// Creates a SavedRoundResult from a RoundResult.
|
||||||
init(from roundResult: RoundResult, netWinnings: Int) {
|
init(from roundResult: RoundResult) {
|
||||||
self.id = roundResult.id
|
self.id = roundResult.id
|
||||||
self.result = roundResult.result.persistenceKey
|
self.result = roundResult.result.persistenceKey
|
||||||
self.playerValue = roundResult.playerValue
|
self.playerValue = roundResult.playerValue
|
||||||
@ -125,7 +101,18 @@ extension SavedRoundResult {
|
|||||||
self.bankerPair = roundResult.bankerPair
|
self.bankerPair = roundResult.bankerPair
|
||||||
self.isNatural = roundResult.isNatural
|
self.isNatural = roundResult.isNatural
|
||||||
self.timestamp = roundResult.timestamp
|
self.timestamp = roundResult.timestamp
|
||||||
self.netWinnings = netWinnings
|
}
|
||||||
|
|
||||||
|
/// Converts back to RoundResult for display.
|
||||||
|
func toRoundResult() -> RoundResult? {
|
||||||
|
guard let gameResult = GameResult(persistenceKey: result) else { return nil }
|
||||||
|
return RoundResult(
|
||||||
|
result: gameResult,
|
||||||
|
playerValue: playerValue,
|
||||||
|
bankerValue: bankerValue,
|
||||||
|
playerPair: playerPair,
|
||||||
|
bankerPair: bankerPair
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,4 +138,3 @@ extension GameResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -131,7 +131,7 @@ struct GameTableView: View {
|
|||||||
RulesHelpView()
|
RulesHelpView()
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showStats) {
|
.sheet(isPresented: $showStats) {
|
||||||
StatisticsSheetView(results: state.roundHistory)
|
StatisticsSheetView(state: state)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showWelcome) {
|
.sheet(isPresented: $showWelcome) {
|
||||||
WelcomeSheet(
|
WelcomeSheet(
|
||||||
|
|||||||
@ -228,7 +228,7 @@ struct SettingsView: View {
|
|||||||
.font(.system(size: Design.BaseFontSize.body))
|
.font(.system(size: Design.BaseFontSize.body))
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
Text("\(gameState.lifetimeStats.roundsPlayed)")
|
Text("\(gameState.aggregatedStats.totalRoundsPlayed)")
|
||||||
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
|
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
@ -240,7 +240,7 @@ struct SettingsView: View {
|
|||||||
.font(.system(size: Design.BaseFontSize.body))
|
.font(.system(size: Design.BaseFontSize.body))
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
let winnings = gameState.lifetimeStats.totalWinnings
|
let winnings = gameState.aggregatedStats.totalWinnings
|
||||||
Text(winnings >= 0 ? "+$\(winnings)" : "-$\(abs(winnings))")
|
Text(winnings >= 0 ? "+$\(winnings)" : "-$\(abs(winnings))")
|
||||||
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
|
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(winnings >= 0 ? .green : .red)
|
.foregroundStyle(winnings >= 0 ? .green : .red)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user