Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
3588c0679f
commit
a7d550eef1
@ -110,6 +110,7 @@ struct GameTableView: View {
|
||||
ChipSelectorView(
|
||||
selectedChip: $selectedChip,
|
||||
balance: state.balance,
|
||||
currentBet: state.totalBetAmount,
|
||||
maxBet: state.maxBet
|
||||
)
|
||||
.frame(maxWidth: isLargeScreen ? maxContentWidth : .infinity)
|
||||
|
||||
@ -101,8 +101,9 @@ final class GameState {
|
||||
}
|
||||
|
||||
/// Whether player can place a bet.
|
||||
/// True if in betting phase, have balance, and haven't hit max bet.
|
||||
var canBet: Bool {
|
||||
currentPhase == .betting && currentBet + settings.minBet <= balance
|
||||
currentPhase == .betting && balance > 0 && currentBet < settings.maxBet
|
||||
}
|
||||
|
||||
/// Whether the current hand can hit.
|
||||
@ -139,9 +140,9 @@ final class GameState {
|
||||
return engine.canSurrender(hand: hand)
|
||||
}
|
||||
|
||||
/// Whether the game is over (out of money).
|
||||
/// Whether the game is over (out of money and no active bet).
|
||||
var isGameOver: Bool {
|
||||
balance < settings.minBet && currentPhase == .betting
|
||||
balance < settings.minBet && currentPhase == .betting && currentBet == 0
|
||||
}
|
||||
|
||||
/// Total rounds played.
|
||||
@ -250,9 +251,14 @@ final class GameState {
|
||||
|
||||
// MARK: - Dealing
|
||||
|
||||
/// Whether the player can deal (betting phase with valid bet).
|
||||
var canDeal: Bool {
|
||||
currentPhase == .betting && currentBet >= settings.minBet
|
||||
}
|
||||
|
||||
/// Deals the initial cards.
|
||||
func deal() async {
|
||||
guard currentBet >= settings.minBet else { return }
|
||||
guard canDeal else { return }
|
||||
|
||||
currentPhase = .dealing
|
||||
playerHands = [BlackjackHand(bet: currentBet)]
|
||||
@ -598,13 +604,20 @@ final class GameState {
|
||||
|
||||
/// Starts a new round.
|
||||
func newRound() {
|
||||
// Reset all hand state
|
||||
playerHands = []
|
||||
dealerHand = BlackjackHand()
|
||||
activeHandIndex = 0
|
||||
|
||||
// Reset bets
|
||||
currentBet = 0
|
||||
insuranceBet = 0
|
||||
|
||||
// Reset UI state
|
||||
showResultBanner = false
|
||||
lastRoundResult = nil
|
||||
currentPhase = .betting
|
||||
|
||||
sound.play(.newRound)
|
||||
}
|
||||
|
||||
|
||||
@ -48,14 +48,16 @@ struct BlackjackHand: Identifiable, Equatable {
|
||||
cards.count == 2 && value == 21 && !isSplit
|
||||
}
|
||||
|
||||
/// Whether this hand can be split (two cards of same rank).
|
||||
/// Whether this hand has a splittable pair (two cards of same rank).
|
||||
/// Note: Additional conditions (balance, max splits, resplit aces) are checked by the engine.
|
||||
var canSplit: Bool {
|
||||
cards.count == 2 && cards[0].rank == cards[1].rank && !isSplit
|
||||
cards.count == 2 && cards[0].rank == cards[1].rank
|
||||
}
|
||||
|
||||
/// Whether this hand can double down.
|
||||
/// Whether this hand has the card count to double down.
|
||||
/// Note: Additional conditions (balance, DAS rule) are checked by the engine.
|
||||
var canDoubleDown: Bool {
|
||||
cards.count == 2 && !isDoubledDown && !isSplit
|
||||
cards.count == 2 && !isDoubledDown
|
||||
}
|
||||
|
||||
/// Whether this hand can hit.
|
||||
|
||||
@ -1299,6 +1299,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Max: $%@" : {
|
||||
"comment" : "A label displaying the maximum bet amount. The argument is the maximum bet amount.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Maximum penetration" : {
|
||||
"comment" : "Description of a deck count option when the user selects 8 decks.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
||||
@ -384,9 +384,15 @@ struct BettingZoneView: View {
|
||||
.font(.system(size: labelFontSize, weight: .bold))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
|
||||
Text(String(localized: "Min: $\(minBet)"))
|
||||
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Text(String(localized: "Min: $\(minBet)"))
|
||||
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
||||
|
||||
Text(String(localized: "Max: $\(maxBet.formatted())"))
|
||||
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +78,7 @@ struct GameTableView: View {
|
||||
TopBarView(
|
||||
balance: state.balance,
|
||||
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil,
|
||||
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
|
||||
onReset: { state.resetGame() },
|
||||
onSettings: { showSettings = true },
|
||||
onHelp: { showRules = true },
|
||||
@ -98,6 +99,7 @@ struct GameTableView: View {
|
||||
ChipSelectorView(
|
||||
selectedChip: $selectedChip,
|
||||
balance: state.balance,
|
||||
currentBet: state.currentBet,
|
||||
maxBet: state.settings.maxBet
|
||||
)
|
||||
.frame(maxWidth: maxContentWidth)
|
||||
@ -195,7 +197,7 @@ struct ActionButtonsView: View {
|
||||
state.clearBet()
|
||||
}
|
||||
|
||||
if state.currentBet >= state.settings.minBet {
|
||||
if state.canDeal {
|
||||
ActionButton(
|
||||
String(localized: "Deal"),
|
||||
icon: "play.fill",
|
||||
|
||||
@ -114,12 +114,6 @@ struct ResultBannerView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
try? await Task.sleep(for: .milliseconds(300))
|
||||
SoundManager.shared.play(.gameOver)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// New Round button
|
||||
Button(action: onNewRound) {
|
||||
@ -171,6 +165,14 @@ struct ResultBannerView: View {
|
||||
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: 0.3)) {
|
||||
showContent = true
|
||||
}
|
||||
|
||||
// Play game over sound if out of chips (after a delay so it doesn't overlap with result sound)
|
||||
if isGameOver {
|
||||
Task {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
SoundManager.shared.play(.gameOver)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel(String(localized: "Round result: \(result.mainHandResult.displayText)"))
|
||||
|
||||
@ -9,20 +9,31 @@ import SwiftUI
|
||||
|
||||
/// A horizontal scrollable chip selector.
|
||||
/// Shows chips based on current balance - higher denomination chips unlock as you win more!
|
||||
/// Chips are disabled when:
|
||||
/// - Balance is less than the chip value
|
||||
/// - Adding the chip would exceed the max bet (considering current bet)
|
||||
public struct ChipSelectorView: View {
|
||||
@Binding var selectedChip: ChipDenomination
|
||||
let balance: Int
|
||||
let currentBet: Int
|
||||
let maxBet: Int
|
||||
let theme: any CasinoTheme
|
||||
|
||||
/// Remaining room before hitting max bet
|
||||
private var remainingBetRoom: Int {
|
||||
maxBet - currentBet
|
||||
}
|
||||
|
||||
public init(
|
||||
selectedChip: Binding<ChipDenomination>,
|
||||
balance: Int,
|
||||
currentBet: Int = 0,
|
||||
maxBet: Int = 100_000,
|
||||
theme: any CasinoTheme = DefaultCasinoTheme()
|
||||
) {
|
||||
self._selectedChip = selectedChip
|
||||
self.balance = balance
|
||||
self.currentBet = currentBet
|
||||
self.maxBet = maxBet
|
||||
self.theme = theme
|
||||
}
|
||||
@ -33,6 +44,11 @@ public struct ChipSelectorView: View {
|
||||
.filter { $0.rawValue <= maxBet } // Don't show chips larger than max bet
|
||||
}
|
||||
|
||||
/// Whether a chip can be used (affordable and within bet limit)
|
||||
private func canUseChip(_ denomination: ChipDenomination) -> Bool {
|
||||
balance >= denomination.rawValue && denomination.rawValue <= remainingBetRoom
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: CasinoDesign.Spacing.medium) {
|
||||
@ -48,8 +64,8 @@ public struct ChipSelectorView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.opacity(balance >= denomination.rawValue ? 1.0 : CasinoDesign.Opacity.medium)
|
||||
.disabled(balance < denomination.rawValue)
|
||||
.opacity(canUseChip(denomination) ? 1.0 : CasinoDesign.Opacity.medium)
|
||||
.disabled(!canUseChip(denomination))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, CasinoDesign.Spacing.large)
|
||||
@ -62,10 +78,23 @@ public struct ChipSelectorView: View {
|
||||
.accessibilityHint(String(localized: "Double tap a chip to select bet amount", bundle: .module))
|
||||
.onChange(of: balance) { _, newBalance in
|
||||
// Auto-select highest affordable chip if current selection is now too expensive
|
||||
if newBalance < selectedChip.rawValue {
|
||||
if let affordable = availableChips.last(where: { newBalance >= $0.rawValue }) {
|
||||
selectedChip = affordable
|
||||
}
|
||||
autoSelectAffordableChip(forBalance: newBalance)
|
||||
}
|
||||
.onChange(of: currentBet) { _, _ in
|
||||
// Auto-select when remaining bet room changes
|
||||
autoSelectAffordableChip(forBalance: balance)
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-selects the highest affordable chip that fits within remaining bet room
|
||||
private func autoSelectAffordableChip(forBalance newBalance: Int) {
|
||||
let newRemainingRoom = maxBet - currentBet
|
||||
// If current selection is unaffordable or exceeds remaining room, find a better chip
|
||||
if newBalance < selectedChip.rawValue || selectedChip.rawValue > newRemainingRoom {
|
||||
if let affordable = availableChips.last(where: {
|
||||
newBalance >= $0.rawValue && $0.rawValue <= newRemainingRoom
|
||||
}) {
|
||||
selectedChip = affordable
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,13 +106,14 @@ public struct ChipSelectorView: View {
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: CasinoDesign.Spacing.xLarge) {
|
||||
Text("Balance: $50,000")
|
||||
Text("Balance: $50,000 | Bet: $1,000 | Max: $5,000")
|
||||
.foregroundStyle(.white)
|
||||
|
||||
ChipSelectorView(
|
||||
selectedChip: .constant(.fiveThousand),
|
||||
selectedChip: .constant(.fiveHundred),
|
||||
balance: 50_000,
|
||||
maxBet: 50_000
|
||||
currentBet: 1_000,
|
||||
maxBet: 5_000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user