CasinoGames/Baccarat/Views/StatisticsSheetView.swift

442 lines
14 KiB
Swift

//
// StatisticsSheetView.swift
// Baccarat
//
// Detailed statistics and scoreboard view.
//
import SwiftUI
import CasinoKit
/// A sheet that displays detailed game statistics and Big Road scoreboard.
struct StatisticsSheetView: View {
let results: [RoundResult]
@Environment(\.dismiss) private var dismiss
// MARK: - Computed Statistics
private var totalRounds: Int { results.count }
private var playerWins: Int {
results.filter { $0.result == .playerWins }.count
}
private var bankerWins: Int {
results.filter { $0.result == .bankerWins }.count
}
private var tieCount: Int {
results.filter { $0.result == .tie }.count
}
private var playerPairs: Int {
results.filter { $0.playerPair }.count
}
private var bankerPairs: Int {
results.filter { $0.bankerPair }.count
}
private var naturals: Int {
results.filter { $0.isNatural }.count
}
private func percentage(_ count: Int) -> String {
guard totalRounds > 0 else { return "0%" }
let pct = Double(count) / Double(totalRounds) * 100
return String(format: "%.0f%%", pct)
}
var body: some View {
SheetContainerView(
title: String(localized: "Statistics"),
content: {
// Summary stats
summarySection
// Win distribution
winDistributionSection
// Side bet frequency
sideBetSection
// Big Road display
bigRoadSection
},
onDone: {
dismiss()
},
doneButtonText: String(localized: "Done")
)
}
// MARK: - Summary Section
private var summarySection: some View {
SheetSection(title: "SESSION SUMMARY", icon: "chart.pie.fill") {
HStack(spacing: Design.Spacing.xLarge) {
StatBox(
value: "\(totalRounds)",
label: String(localized: "Rounds"),
color: .white
)
StatBox(
value: "\(naturals)",
label: String(localized: "Naturals"),
color: .yellow
)
}
.frame(maxWidth: .infinity)
}
}
// MARK: - Win Distribution Section
private var winDistributionSection: some View {
SheetSection(title: "WIN DISTRIBUTION", icon: "trophy.fill") {
VStack(spacing: Design.Spacing.medium) {
HStack(spacing: Design.Spacing.medium) {
WinStatView(
title: String(localized: "Player"),
count: playerWins,
percentage: percentage(playerWins),
color: .blue
)
WinStatView(
title: String(localized: "Tie"),
count: tieCount,
percentage: percentage(tieCount),
color: .green
)
WinStatView(
title: String(localized: "Banker"),
count: bankerWins,
percentage: percentage(bankerWins),
color: .red
)
}
// Win bar visualization
if totalRounds > 0 {
WinDistributionBar(
playerWins: playerWins,
tieCount: tieCount,
bankerWins: bankerWins
)
.frame(height: Design.Spacing.large)
.clipShape(.rect(cornerRadius: Design.CornerRadius.small))
}
}
}
}
// MARK: - Side Bet Section
private var sideBetSection: some View {
SheetSection(title: "SIDE BET FREQUENCY", icon: "sparkles") {
HStack(spacing: Design.Spacing.xLarge) {
PairStatView(
title: String(localized: "P Pair"),
count: playerPairs,
percentage: percentage(playerPairs),
color: .blue
)
PairStatView(
title: String(localized: "B Pair"),
count: bankerPairs,
percentage: percentage(bankerPairs),
color: .red
)
}
.frame(maxWidth: .infinity)
}
}
// MARK: - Big Road Section
private var bigRoadSection: some View {
SheetSection(title: "BIG ROAD", icon: "chart.bar.xaxis") {
if results.isEmpty {
Text(String(localized: "No rounds played yet"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.secondary))
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.xLarge)
} else {
BigRoadView(results: results)
.frame(height: Design.Size.bigRoadHeight)
}
}
}
}
// MARK: - Supporting Views
/// A box displaying a single statistic.
private struct StatBox: View {
let value: String
let label: String
let color: Color
var body: some View {
VStack(spacing: Design.Spacing.xSmall) {
Text(value)
.font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded))
.foregroundStyle(color)
Text(label)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.secondary))
}
.frame(minWidth: Design.Size.statBoxMinWidth)
.padding(Design.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.black.opacity(Design.Opacity.overlay))
)
}
}
/// A win stat display with count and percentage.
private struct WinStatView: View {
let title: String
let count: Int
let percentage: String
let color: Color
var body: some View {
VStack(spacing: Design.Spacing.xSmall) {
Circle()
.fill(color)
.frame(width: Design.Size.winIndicatorSize, height: Design.Size.winIndicatorSize)
Text("\(count)")
.font(.system(size: Design.BaseFontSize.title, weight: .bold))
.foregroundStyle(.white)
Text(percentage)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.secondary))
Text(title)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(color)
}
.frame(maxWidth: .infinity)
}
}
/// A pair stat display.
private struct PairStatView: View {
let title: String
let count: Int
let percentage: String
let color: Color
var body: some View {
VStack(spacing: Design.Spacing.xSmall) {
Text("\(count)")
.font(.system(size: Design.BaseFontSize.title, weight: .bold))
.foregroundStyle(color)
Text(percentage)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.secondary))
Text(title)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.accent))
}
}
}
/// A horizontal bar showing win distribution.
private struct WinDistributionBar: View {
let playerWins: Int
let tieCount: Int
let bankerWins: Int
private var total: Int { playerWins + tieCount + bankerWins }
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
if playerWins > 0 {
Rectangle()
.fill(Color.blue)
.frame(width: geometry.size.width * CGFloat(playerWins) / CGFloat(total))
}
if tieCount > 0 {
Rectangle()
.fill(Color.green)
.frame(width: geometry.size.width * CGFloat(tieCount) / CGFloat(total))
}
if bankerWins > 0 {
Rectangle()
.fill(Color.red)
.frame(width: geometry.size.width * CGFloat(bankerWins) / CGFloat(total))
}
}
}
}
}
/// The Big Road scoreboard - a grid showing result patterns.
/// Results are arranged in columns, with each column representing a streak of same results.
private struct BigRoadView: View {
let results: [RoundResult]
private let maxRows = 6
private let cellSize: CGFloat = Design.Size.bigRoadCellSize
/// Convert results into columns for Big Road display.
private var columns: [[RoundResult]] {
var cols: [[RoundResult]] = []
var currentCol: [RoundResult] = []
var lastResult: GameResult?
for result in results {
// Skip ties for column tracking (ties go in the current column)
let currentResult = result.result
if currentResult == .tie {
// Ties don't start new columns, they go with the current streak
if !currentCol.isEmpty {
currentCol.append(result)
} else if !cols.isEmpty {
cols[cols.count - 1].append(result)
} else {
currentCol.append(result)
}
} else if lastResult == nil || currentResult == lastResult {
// Same as last or first result - continue column
currentCol.append(result)
lastResult = currentResult
} else {
// Different result - start new column
if !currentCol.isEmpty {
cols.append(currentCol)
}
currentCol = [result]
lastResult = currentResult
}
}
// Add remaining column
if !currentCol.isEmpty {
cols.append(currentCol)
}
return cols
}
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: Design.Spacing.xxSmall) {
ForEach(Array(columns.enumerated()), id: \.offset) { _, column in
VStack(spacing: Design.Spacing.xxSmall) {
ForEach(Array(column.prefix(maxRows).enumerated()), id: \.offset) { _, result in
BigRoadCell(result: result)
}
// If column has more than maxRows, show overflow count
if column.count > maxRows {
Text("+\(column.count - maxRows)")
.font(.system(size: Design.BaseFontSize.xxSmall))
.foregroundStyle(.white.opacity(Design.Opacity.secondary))
}
Spacer(minLength: 0)
}
}
}
.padding(Design.Spacing.small)
}
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.black.opacity(Design.Opacity.overlay))
)
.scrollIndicators(.hidden)
}
}
/// A single cell in the Big Road display.
private struct BigRoadCell: View {
let result: RoundResult
private let cellSize: CGFloat = Design.Size.bigRoadCellSize
private var color: Color {
switch result.result {
case .playerWins: return .blue
case .bankerWins: return .red
case .tie: return .green
}
}
var body: some View {
ZStack {
// Main circle
Circle()
.stroke(color, lineWidth: Design.LineWidth.medium)
.frame(width: cellSize, height: cellSize)
// Pair indicator (small dot at bottom)
if result.hasPair {
Circle()
.fill(Color.yellow)
.frame(width: cellSize * 0.25, height: cellSize * 0.25)
.offset(y: cellSize * 0.3)
}
// Natural indicator (small dot at top)
if result.isNatural {
Circle()
.fill(Color.white)
.frame(width: cellSize * 0.25, height: cellSize * 0.25)
.offset(y: -cellSize * 0.3)
}
// Tie diagonal line if it's a tie
if result.result == .tie {
Rectangle()
.fill(color)
.frame(width: cellSize * 0.8, height: Design.LineWidth.medium)
.rotationEffect(.degrees(-45))
}
}
}
}
// MARK: - Design Constants Extensions
extension Design.Size {
static let bigRoadHeight: CGFloat = 200
static let bigRoadCellSize: CGFloat = 24
static let statBoxMinWidth: CGFloat = 80
static let winIndicatorSize: CGFloat = 24
}
// MARK: - Preview
#Preview {
StatisticsSheetView(results: [
RoundResult(result: .playerWins, playerValue: 8, bankerValue: 6, playerPair: true),
RoundResult(result: .playerWins, playerValue: 7, bankerValue: 5),
RoundResult(result: .bankerWins, playerValue: 4, bankerValue: 7),
RoundResult(result: .bankerWins, playerValue: 3, bankerValue: 8, bankerPair: true),
RoundResult(result: .bankerWins, playerValue: 2, bankerValue: 6),
RoundResult(result: .tie, playerValue: 5, bankerValue: 5),
RoundResult(result: .playerWins, playerValue: 9, bankerValue: 3),
RoundResult(result: .bankerWins, playerValue: 2, bankerValue: 8, playerPair: true, bankerPair: true)
])
}