469 lines
16 KiB
Swift
469 lines
16 KiB
Swift
//
|
|
// RulesHelpView.swift
|
|
// Baccarat
|
|
//
|
|
// A paginated help view explaining game rules and side bets.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// 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
|
|
Color.black.opacity(0.9)
|
|
.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: 32))
|
|
|
|
Text("BACCARAT")
|
|
.font(.system(size: 28, 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: 22, 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(red: 0.15, green: 0.35, blue: 0.55))
|
|
)
|
|
.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: 0.3)) {
|
|
goToPreviousPage()
|
|
}
|
|
} label: {
|
|
Image(systemName: "chevron.left.circle.fill")
|
|
.font(.system(size: 36))
|
|
.foregroundStyle(currentPage.rawValue > 0 ? .yellow : .gray.opacity(0.5))
|
|
}
|
|
.disabled(currentPage.rawValue == 0)
|
|
|
|
// Back to game button
|
|
Button {
|
|
dismiss()
|
|
} label: {
|
|
Text("BACK TO GAME")
|
|
.font(.system(size: 16, 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: 0.3)) {
|
|
goToNextPage()
|
|
}
|
|
} label: {
|
|
Image(systemName: "chevron.right.circle.fill")
|
|
.font(.system(size: 36))
|
|
.foregroundStyle(currentPage.rawValue < RulesPage.allCases.count - 1 ? .green : .gray.opacity(0.5))
|
|
}
|
|
.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(0.3))
|
|
|
|
Text("Card Values")
|
|
.font(.system(size: 18, 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(0.3))
|
|
|
|
Text("Natural Win")
|
|
.font(.system(size: 18, 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(0.3))
|
|
|
|
Text("Player Rules")
|
|
.font(.system(size: 18, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
|
|
RuleSection(items: [
|
|
"0-5: Player draws a third card",
|
|
"6-7: Player stands"
|
|
])
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(0.3))
|
|
|
|
Text("Banker Rules")
|
|
.font(.system(size: 18, 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(0.2))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct BankerRuleRow: View {
|
|
let bankerTotal: String
|
|
let action: String
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text("Banker \(bankerTotal):")
|
|
.font(.system(size: 13, weight: .semibold))
|
|
.foregroundStyle(.yellow)
|
|
.frame(width: 80, alignment: .leading)
|
|
|
|
Text(action)
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(.white.opacity(0.9))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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(0.3))
|
|
|
|
Text("Payout Table")
|
|
.font(.system(size: 18, 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(0.2))
|
|
)
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(0.3))
|
|
|
|
Text("Important")
|
|
.font(.system(size: 18, 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: 14))
|
|
.foregroundStyle(.white.opacity(0.9))
|
|
|
|
Spacer()
|
|
|
|
Text(payout)
|
|
.font(.system(size: 14, 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(0.3))
|
|
|
|
Text("Payout")
|
|
.font(.system(size: 18, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
|
|
HStack {
|
|
VStack {
|
|
Text("11:1")
|
|
.font(.system(size: 48, weight: .black, design: .rounded))
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [.yellow, .orange],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
|
|
Text("Pair Pays")
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundStyle(.white.opacity(0.7))
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.padding(.vertical, Design.Spacing.medium)
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(0.3))
|
|
|
|
Text("Examples")
|
|
.font(.system(size: 18, 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(0.3))
|
|
|
|
Text("Tips")
|
|
.font(.system(size: 18, 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: 16, weight: .semibold))
|
|
.foregroundStyle(.yellow)
|
|
}
|
|
|
|
if let text = text {
|
|
Text(text)
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(.white.opacity(0.9))
|
|
.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(0.9))
|
|
}
|
|
.font(.system(size: 14))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RulesHelpView()
|
|
}
|
|
|