CasinoGames/Blackjack/Views/Sheets/StatisticsSheetView.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()))
}