CasinoGames/Baccarat/Views/RulesHelpView.swift

472 lines
17 KiB
Swift

//
// RulesHelpView.swift
// Baccarat
//
// A paginated help view explaining game rules and side bets.
//
import SwiftUI
import CasinoKit
/// The available rule pages.
enum RulesPage: Int, CaseIterable, Identifiable {
case basicRules = 0
case thirdCardRules = 1
case dragonBonus = 2
case pairBonus = 3
var id: Int { rawValue }
var title: String {
switch self {
case .basicRules: return String(localized: "How to Play")
case .thirdCardRules: return String(localized: "Third Card Rules")
case .dragonBonus: return String(localized: "Dragon Bonus")
case .pairBonus: return String(localized: "Pair Bonus")
}
}
}
/// A multi-page help view explaining baccarat rules.
struct RulesHelpView: View {
@Environment(\.dismiss) private var dismiss
@State private var currentPage: RulesPage = .basicRules
// MARK: - Layout Constants
private let modalCornerRadius = Design.CornerRadius.xxxLarge
private let contentCornerRadius = Design.CornerRadius.xLarge
private let contentPadding = Design.Spacing.large
private let buttonSize: CGFloat = 44
// MARK: - Body
var body: some View {
ZStack {
// Background - same as other sheets
Color.Settings.background
.ignoresSafeArea()
VStack(spacing: Design.Spacing.medium) {
// Header with logo
headerView
// Content card
contentCard
.padding(.horizontal)
// Navigation
navigationView
.padding(.bottom, Design.Spacing.large)
}
}
}
// MARK: - Subviews
private var headerView: some View {
VStack(spacing: Design.Spacing.small) {
// Cards icon
HStack(spacing: -Design.Spacing.small) {
Image(systemName: "suit.spade.fill")
.foregroundStyle(.white)
Image(systemName: "suit.heart.fill")
.foregroundStyle(.red)
}
.font(.system(size: Design.BaseFontSize.title))
Text("BACCARAT")
.font(.system(size: Design.BaseFontSize.title - Design.Spacing.xSmall, weight: .black, design: .rounded))
.foregroundStyle(
LinearGradient(
colors: [.yellow, .orange],
startPoint: .top,
endPoint: .bottom
)
)
.tracking(3)
}
.padding(.top, Design.Spacing.xLarge)
}
private var contentCard: some View {
VStack(spacing: 0) {
// Page title
Text(currentPage.title)
.font(.system(size: Design.BaseFontSize.xxLarge + Design.Spacing.xxSmall, weight: .bold, design: .rounded))
.foregroundStyle(.yellow)
.padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.medium)
// Scrollable content
ScrollView {
pageContent
.padding(.horizontal, contentPadding)
.padding(.bottom, Design.Spacing.large)
}
}
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: contentCornerRadius)
.fill(Color.white.opacity(Design.Opacity.verySubtle))
)
.clipShape(RoundedRectangle(cornerRadius: contentCornerRadius))
}
@ViewBuilder
private var pageContent: some View {
switch currentPage {
case .basicRules:
BasicRulesContent()
case .thirdCardRules:
ThirdCardRulesContent()
case .dragonBonus:
DragonBonusContent()
case .pairBonus:
PairBonusContent()
}
}
private var navigationView: some View {
HStack(spacing: Design.Spacing.medium) {
// Previous button
Button {
withAnimation(.spring(duration: Design.Animation.quick)) {
goToPreviousPage()
}
} label: {
Image(systemName: "chevron.left.circle.fill")
.font(.system(size: Design.BaseFontSize.largeTitle))
.foregroundStyle(currentPage.rawValue > 0 ? .yellow : .gray.opacity(Design.Opacity.medium))
}
.disabled(currentPage.rawValue == 0)
// Back to game button
Button {
dismiss()
} label: {
Text("BACK TO GAME")
.font(.system(size: Design.BaseFontSize.large, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
)
}
// Next button
Button {
withAnimation(.spring(duration: Design.Animation.quick)) {
goToNextPage()
}
} label: {
Image(systemName: "chevron.right.circle.fill")
.font(.system(size: Design.BaseFontSize.largeTitle))
.foregroundStyle(currentPage.rawValue < RulesPage.allCases.count - 1 ? .green : .gray.opacity(Design.Opacity.medium))
}
.disabled(currentPage.rawValue >= RulesPage.allCases.count - 1)
}
}
// MARK: - Navigation
private func goToPreviousPage() {
if currentPage.rawValue > 0 {
currentPage = RulesPage(rawValue: currentPage.rawValue - 1) ?? .basicRules
}
}
private func goToNextPage() {
if currentPage.rawValue < RulesPage.allCases.count - 1 {
currentPage = RulesPage(rawValue: currentPage.rawValue + 1) ?? .pairBonus
}
}
}
// MARK: - Basic Rules Content
private struct BasicRulesContent: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
RuleSection(text: "Two hands are dealt: one for the Player and one for the Banker. You may bet on which hand will win, or that they will tie.")
RuleSection(title: "Payouts", items: [
"Player wins: pays 1 to 1",
"Banker wins: pays 0.95 to 1 (5% commission)",
"Tie: pays 8 to 1"
])
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Card Values")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
RuleSection(items: [
"Aces = 1 point",
"2-9 = Face value",
"10, J, Q, K = 0 points"
])
RuleSection(text: "Hand values are the sum of cards, keeping only the last digit. For example: 7 + 8 = 15, so the hand value is 5.")
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Natural Win")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
RuleSection(text: "If either hand totals 8 or 9 with the first two cards, it's a \"Natural\" and the round ends immediately.")
}
}
}
// MARK: - Third Card Rules Content
private struct ThirdCardRulesContent: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
RuleSection(text: "If neither hand has a Natural, additional cards may be drawn according to fixed rules.")
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Player Rules")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
RuleSection(items: [
"0-5: Player draws a third card",
"6-7: Player stands"
])
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Banker Rules")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
RuleSection(text: "If Player stood (6-7), Banker draws on 0-5 and stands on 6-7.")
RuleSection(text: "If Player drew a third card, Banker's action depends on both the Banker's total and the Player's third card:")
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
BankerRuleRow(bankerTotal: "0-2", action: "Always draws")
BankerRuleRow(bankerTotal: "3", action: "Draws unless Player's 3rd was 8")
BankerRuleRow(bankerTotal: "4", action: "Draws if Player's 3rd was 2-7")
BankerRuleRow(bankerTotal: "5", action: "Draws if Player's 3rd was 4-7")
BankerRuleRow(bankerTotal: "6", action: "Draws if Player's 3rd was 6-7")
BankerRuleRow(bankerTotal: "7", action: "Always stands")
}
.padding()
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.black.opacity(Design.Opacity.hint))
)
}
}
}
private struct BankerRuleRow: View {
let bankerTotal: String
let action: String
private let labelWidth: CGFloat = 80
var body: some View {
HStack {
Text("Banker \(bankerTotal):")
.font(.system(size: Design.BaseFontSize.callout, weight: .semibold))
.foregroundStyle(.yellow)
.frame(width: labelWidth, alignment: .leading)
Text(action)
.font(.system(size: Design.BaseFontSize.callout))
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
}
}
}
// MARK: - Dragon Bonus Content
private struct DragonBonusContent: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
RuleSection(text: "The Dragon Bonus is a side bet available for both Player and Banker. It pays based on how the winning hand wins.")
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Payout Table")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
VStack(spacing: Design.Spacing.xSmall) {
PayoutRow(condition: "Natural Win (8 or 9)", payout: "1 to 1")
PayoutRow(condition: "Win by 9 points", payout: "30 to 1")
PayoutRow(condition: "Win by 8 points", payout: "10 to 1")
PayoutRow(condition: "Win by 7 points", payout: "6 to 1")
PayoutRow(condition: "Win by 6 points", payout: "4 to 1")
PayoutRow(condition: "Win by 5 points", payout: "2 to 1")
PayoutRow(condition: "Win by 4 points", payout: "1 to 1")
}
.padding()
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.black.opacity(Design.Opacity.hint))
)
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Important")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
RuleSection(items: [
"Dragon Bonus loses if your side doesn't win",
"Dragon Bonus loses on a tie",
"Wins by less than 4 points also lose"
])
}
}
}
private struct PayoutRow: View {
let condition: String
let payout: String
var body: some View {
HStack {
Text(condition)
.font(.system(size: Design.BaseFontSize.medium))
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
Spacer()
Text(payout)
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(.yellow)
}
.padding(.vertical, Design.Spacing.xxSmall)
}
}
// MARK: - Pair Bonus Content
private struct PairBonusContent: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
RuleSection(text: "Pair Bonus bets are available for both Player and Banker. They pay when the first two cards dealt to that hand form a pair.")
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Payout")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
HStack {
VStack {
Text("11:1")
.font(.system(size: Design.BaseFontSize.largeTitle + Design.Spacing.medium, weight: .black, design: .rounded))
.foregroundStyle(
LinearGradient(
colors: [.yellow, .orange],
startPoint: .top,
endPoint: .bottom
)
)
Text("Pair Pays")
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
}
.frame(maxWidth: .infinity)
}
.padding(.vertical, Design.Spacing.medium)
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Examples")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
RuleSection(items: [
"5♥ + 5♣ = Pair (wins 11:1)",
"J♦ + J♠ = Pair (wins 11:1)",
"A♥ + A♥ = Pair (wins 11:1)"
])
RuleSection(text: "Note: Suits are disregarded. Only the rank matters for a pair.")
Divider()
.background(Color.white.opacity(Design.Opacity.light))
Text("Tips")
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
RuleSection(items: [
"Pair bets are independent of the main game result",
"You can bet on Player Pair, Banker Pair, or both",
"Pairs occur roughly once every 15 hands"
])
}
}
}
// MARK: - Helper Views
private struct RuleSection: View {
var title: String? = nil
var text: String? = nil
var items: [String]? = nil
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
if let title = title {
Text(title)
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(.yellow)
}
if let text = text {
Text(text)
.font(.system(size: Design.BaseFontSize.medium))
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
.fixedSize(horizontal: false, vertical: true)
}
if let items = items {
ForEach(items, id: \.self) { item in
HStack(alignment: .top, spacing: Design.Spacing.small) {
Text("")
.foregroundStyle(.yellow)
Text(item)
.foregroundStyle(.white.opacity(Design.Opacity.almostFull))
}
.font(.system(size: Design.BaseFontSize.medium))
}
}
}
}
}
#Preview {
RulesHelpView()
}