229 lines
8.5 KiB
Swift
229 lines
8.5 KiB
Swift
//
|
|
// StatisticsSheetView.swift
|
|
// Blackjack
|
|
//
|
|
// Game statistics and history.
|
|
//
|
|
|
|
import SwiftUI
|
|
import CasinoKit
|
|
|
|
struct StatisticsSheetView: View {
|
|
let state: GameState
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
// MARK: - Computed Stats
|
|
|
|
private var totalRounds: Int {
|
|
state.roundHistory.count
|
|
}
|
|
|
|
private var wins: Int {
|
|
state.roundHistory.filter { $0.mainHandResult.isWin }.count
|
|
}
|
|
|
|
private var losses: Int {
|
|
state.roundHistory.filter {
|
|
$0.mainHandResult == .lose || $0.mainHandResult == .bust
|
|
}.count
|
|
}
|
|
|
|
private var pushes: Int {
|
|
state.roundHistory.filter { $0.mainHandResult == .push }.count
|
|
}
|
|
|
|
private var blackjacks: Int {
|
|
state.roundHistory.filter { $0.mainHandResult == .blackjack }.count
|
|
}
|
|
|
|
private var busts: Int {
|
|
state.roundHistory.filter { $0.mainHandResult == .bust }.count
|
|
}
|
|
|
|
private var surrenders: Int {
|
|
state.roundHistory.filter { $0.mainHandResult == .surrender }.count
|
|
}
|
|
|
|
private var winRate: Double {
|
|
guard totalRounds > 0 else { return 0 }
|
|
return Double(wins) / Double(totalRounds) * 100
|
|
}
|
|
|
|
private var totalWinnings: Int {
|
|
state.roundHistory.reduce(0) { $0 + $1.totalWinnings }
|
|
}
|
|
|
|
private var biggestWin: Int {
|
|
state.roundHistory.map { $0.totalWinnings }.filter { $0 > 0 }.max() ?? 0
|
|
}
|
|
|
|
private var biggestLoss: Int {
|
|
state.roundHistory.map { $0.totalWinnings }.filter { $0 < 0 }.min() ?? 0
|
|
}
|
|
|
|
var body: some View {
|
|
SheetContainerView(
|
|
title: String(localized: "Statistics"),
|
|
content: {
|
|
// Session Summary
|
|
SheetSection(title: String(localized: "SESSION SUMMARY"), icon: "chart.bar.fill") {
|
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: Design.Spacing.medium) {
|
|
StatBox(title: String(localized: "Rounds"), value: "\(totalRounds)", color: .white)
|
|
StatBox(title: String(localized: "Win Rate"), value: formatPercent(winRate), color: winRate >= 50 ? .green : .orange)
|
|
StatBox(title: String(localized: "Net"), value: formatMoney(totalWinnings), color: totalWinnings >= 0 ? .green : .red)
|
|
StatBox(title: String(localized: "Balance"), value: "$\(state.balance)", color: Color.Settings.accent)
|
|
}
|
|
}
|
|
|
|
// Win Distribution
|
|
SheetSection(title: String(localized: "OUTCOMES"), icon: "chart.pie.fill") {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
OutcomeRow(label: String(localized: "Blackjacks"), count: blackjacks, total: totalRounds, color: .yellow)
|
|
OutcomeRow(label: String(localized: "Wins"), count: wins - blackjacks, total: totalRounds, color: .green)
|
|
OutcomeRow(label: String(localized: "Pushes"), count: pushes, total: totalRounds, color: .blue)
|
|
OutcomeRow(label: String(localized: "Losses"), count: losses - busts, total: totalRounds, color: .orange)
|
|
OutcomeRow(label: String(localized: "Busts"), count: busts, total: totalRounds, color: .red)
|
|
if surrenders > 0 {
|
|
OutcomeRow(label: String(localized: "Surrenders"), count: surrenders, total: totalRounds, color: .gray)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Biggest Swings
|
|
if totalRounds > 0 {
|
|
SheetSection(title: String(localized: "BIGGEST SWINGS"), icon: "arrow.up.arrow.down") {
|
|
HStack(spacing: Design.Spacing.large) {
|
|
VStack(spacing: Design.Spacing.xSmall) {
|
|
Text(String(localized: "Best"))
|
|
.font(.system(size: Design.BaseFontSize.small))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
Text(formatMoney(biggestWin))
|
|
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.green)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
|
|
Divider()
|
|
.frame(height: 40)
|
|
.background(Color.white.opacity(Design.Opacity.hint))
|
|
|
|
VStack(spacing: Design.Spacing.xSmall) {
|
|
Text(String(localized: "Worst"))
|
|
.font(.system(size: Design.BaseFontSize.small))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
Text(formatMoney(biggestLoss))
|
|
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.red)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onCancel: nil,
|
|
onDone: { dismiss() },
|
|
doneButtonText: String(localized: "Done")
|
|
)
|
|
}
|
|
|
|
private func formatMoney(_ amount: Int) -> String {
|
|
if amount >= 0 {
|
|
return "+$\(amount)"
|
|
} else {
|
|
return "-$\(abs(amount))"
|
|
}
|
|
}
|
|
|
|
private func formatPercent(_ value: Double) -> String {
|
|
value.formatted(.number.precision(.fractionLength(1))) + "%"
|
|
}
|
|
}
|
|
|
|
// MARK: - Stat Box
|
|
|
|
struct StatBox: View {
|
|
let title: String
|
|
let value: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.xSmall) {
|
|
Text(title)
|
|
.font(.system(size: Design.BaseFontSize.small))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
|
|
Text(value)
|
|
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded))
|
|
.foregroundStyle(color)
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.7)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(Design.Spacing.medium)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
|
.fill(Color.white.opacity(Design.Opacity.subtle))
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Outcome Row
|
|
|
|
struct OutcomeRow: View {
|
|
let label: String
|
|
let count: Int
|
|
let total: Int
|
|
let color: Color
|
|
|
|
private var percentage: Double {
|
|
guard total > 0 else { return 0 }
|
|
return Double(count) / Double(total) * 100
|
|
}
|
|
|
|
private func formatPercentWhole(_ value: Double) -> String {
|
|
value.formatted(.number.precision(.fractionLength(0))) + "%"
|
|
}
|
|
|
|
var body: some View {
|
|
HStack {
|
|
// Label
|
|
Text(label)
|
|
.font(.system(size: Design.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
|
|
|
Spacer()
|
|
|
|
// Count
|
|
Text("\(count)")
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .bold))
|
|
.foregroundStyle(color)
|
|
|
|
// Progress bar
|
|
GeometryReader { geometry in
|
|
ZStack(alignment: .leading) {
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.xSmall)
|
|
.fill(Color.white.opacity(Design.Opacity.subtle))
|
|
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.xSmall)
|
|
.fill(color)
|
|
.frame(width: geometry.size.width * CGFloat(percentage / 100))
|
|
}
|
|
}
|
|
.frame(width: 60, height: 8)
|
|
|
|
// Percentage
|
|
Text(formatPercentWhole(percentage))
|
|
.font(.system(size: Design.BaseFontSize.small, design: .rounded))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
.frame(width: 40, alignment: .trailing)
|
|
}
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
StatisticsSheetView(state: GameState(settings: GameSettings()))
|
|
}
|
|
|