Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
a0ac5a6e64
commit
7e16e67826
@ -411,7 +411,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -443,7 +443,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
||||
@ -403,96 +403,99 @@ final class BlackjackEngine {
|
||||
let dealerValue = dealerUpCard.blackjackValue
|
||||
let isSoft = playerHand.isSoft
|
||||
|
||||
// Check for count-based deviations from basic strategy
|
||||
// Helper to format true count with sign
|
||||
let tcDisplay = tc >= 0 ? "+\(tc)" : "\(tc)"
|
||||
|
||||
// 16 vs 10: Stand at TC 0+ (basic says Hit)
|
||||
// Check for count-based deviations from basic strategy (Illustrious 18)
|
||||
|
||||
// 16 vs 10: Stand when TC ≥ 0 (basic strategy says Hit)
|
||||
if playerValue == 16 && !isSoft && dealerValue == 10 {
|
||||
if tc >= 0 {
|
||||
return String(localized: "Stand (Count: 16v10 at TC≥0)")
|
||||
return String(localized: "Stand instead of Hit (TC \(tcDisplay), deck is neutral/rich)")
|
||||
}
|
||||
}
|
||||
|
||||
// 15 vs 10: Stand at TC +4+ (basic says Hit)
|
||||
// 15 vs 10: Stand when TC ≥ +4 (basic strategy says Hit/Surrender)
|
||||
if playerValue == 15 && !isSoft && dealerValue == 10 {
|
||||
if tc >= 4 {
|
||||
return String(localized: "Stand (Count: 15v10 at TC≥+4)")
|
||||
return String(localized: "Stand instead of Hit (TC \(tcDisplay), deck is very rich)")
|
||||
}
|
||||
}
|
||||
|
||||
// 12 vs 2: Stand at TC +3+ (basic says Hit)
|
||||
// 12 vs 2: Stand when TC ≥ +3 (basic strategy says Hit)
|
||||
if playerValue == 12 && !isSoft && dealerValue == 2 {
|
||||
if tc >= 3 {
|
||||
return String(localized: "Stand (Count: 12v2 at TC≥+3)")
|
||||
return String(localized: "Stand instead of Hit (TC \(tcDisplay), dealer likely to bust)")
|
||||
}
|
||||
}
|
||||
|
||||
// 12 vs 3: Stand at TC +2+ (basic says Hit)
|
||||
// 12 vs 3: Stand when TC ≥ +2 (basic strategy says Hit)
|
||||
if playerValue == 12 && !isSoft && dealerValue == 3 {
|
||||
if tc >= 2 {
|
||||
return String(localized: "Stand (Count: 12v3 at TC≥+2)")
|
||||
return String(localized: "Stand instead of Hit (TC \(tcDisplay), dealer likely to bust)")
|
||||
}
|
||||
}
|
||||
|
||||
// 12 vs 4: Hit at TC < 0 (basic says Stand)
|
||||
// 12 vs 4: Hit when TC < 0 (basic strategy says Stand)
|
||||
if playerValue == 12 && !isSoft && dealerValue == 4 {
|
||||
if tc < 0 {
|
||||
return String(localized: "Hit (Count: 12v4 at TC<0)")
|
||||
return String(localized: "Hit instead of Stand (TC \(tcDisplay), deck is poor)")
|
||||
}
|
||||
}
|
||||
|
||||
// 13 vs 2: Hit at TC < -1 (basic says Stand)
|
||||
// 13 vs 2: Hit when TC < -1 (basic strategy says Stand)
|
||||
if playerValue == 13 && !isSoft && dealerValue == 2 {
|
||||
if tc < -1 {
|
||||
return String(localized: "Hit (Count: 13v2 at TC<-1)")
|
||||
return String(localized: "Hit instead of Stand (TC \(tcDisplay), deck is very poor)")
|
||||
}
|
||||
}
|
||||
|
||||
// 16 vs 9: Stand at TC +5+ (basic says Hit)
|
||||
// 16 vs 9: Stand when TC ≥ +5 (basic strategy says Hit)
|
||||
if playerValue == 16 && !isSoft && dealerValue == 9 {
|
||||
if tc >= 5 {
|
||||
return String(localized: "Stand (Count: 16v9 at TC≥+5)")
|
||||
return String(localized: "Stand instead of Hit (TC \(tcDisplay), deck is extremely rich)")
|
||||
}
|
||||
}
|
||||
|
||||
// 10 vs 10: Double at TC +4+ (basic says Hit)
|
||||
// 10 vs 10: Double when TC ≥ +4 (basic strategy says Hit)
|
||||
if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 10 {
|
||||
if tc >= 4 {
|
||||
return String(localized: "Double (Count: 10v10 at TC≥+4)")
|
||||
return String(localized: "Double instead of Hit (TC \(tcDisplay), high cards favor you)")
|
||||
}
|
||||
}
|
||||
|
||||
// 10 vs A: Double at TC +4+ (basic says Hit)
|
||||
// 10 vs Ace: Double when TC ≥ +4 (basic strategy says Hit)
|
||||
if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 1 {
|
||||
if tc >= 4 {
|
||||
return String(localized: "Double (Count: 10vA at TC≥+4)")
|
||||
return String(localized: "Double instead of Hit (TC \(tcDisplay), high cards favor you)")
|
||||
}
|
||||
}
|
||||
|
||||
// 9 vs 2: Double at TC +1+ (basic says Hit)
|
||||
// 9 vs 2: Double when TC ≥ +1 (basic strategy says Hit)
|
||||
if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 2 {
|
||||
if tc >= 1 {
|
||||
return String(localized: "Double (Count: 9v2 at TC≥+1)")
|
||||
return String(localized: "Double instead of Hit (TC \(tcDisplay), slight edge to double)")
|
||||
}
|
||||
}
|
||||
|
||||
// 9 vs 7: Double at TC +3+ (basic says Hit)
|
||||
// 9 vs 7: Double when TC ≥ +3 (basic strategy says Hit)
|
||||
if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 7 {
|
||||
if tc >= 3 {
|
||||
return String(localized: "Double (Count: 9v7 at TC≥+3)")
|
||||
return String(localized: "Double instead of Hit (TC \(tcDisplay), deck favors doubling)")
|
||||
}
|
||||
}
|
||||
|
||||
// Pair of 10s vs 5: Split at TC +5+ (basic says Stand)
|
||||
// Pair of 10s vs 5: Split when TC ≥ +5 (basic strategy says Stand)
|
||||
if playerHand.canSplit && playerHand.cards[0].blackjackValue == 10 && dealerValue == 5 {
|
||||
if tc >= 5 {
|
||||
return String(localized: "Split (Count: 10,10v5 at TC≥+5)")
|
||||
return String(localized: "Split instead of Stand (TC \(tcDisplay), dealer very likely to bust)")
|
||||
}
|
||||
}
|
||||
|
||||
// Pair of 10s vs 6: Split at TC +4+ (basic says Stand)
|
||||
// Pair of 10s vs 6: Split when TC ≥ +4 (basic strategy says Stand)
|
||||
if playerHand.canSplit && playerHand.cards[0].blackjackValue == 10 && dealerValue == 6 {
|
||||
if tc >= 4 {
|
||||
return String(localized: "Split (Count: 10,10v6 at TC≥+4)")
|
||||
return String(localized: "Split instead of Stand (TC \(tcDisplay), dealer very likely to bust)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -661,6 +661,7 @@ final class GameState {
|
||||
var roundWinnings = 0
|
||||
var wasBlackjack = false
|
||||
var hadBust = false
|
||||
var perHandWinnings: [Int] = []
|
||||
|
||||
// Evaluate each hand
|
||||
for i in 0..<playerHands.count {
|
||||
@ -677,8 +678,12 @@ final class GameState {
|
||||
result: result,
|
||||
isDoubled: playerHands[i].isDoubledDown
|
||||
)
|
||||
let totalBet = playerHands[i].bet * (playerHands[i].isDoubledDown ? 2 : 1)
|
||||
let handWinnings = payout - totalBet
|
||||
|
||||
balance += payout
|
||||
roundWinnings += payout - playerHands[i].bet * (playerHands[i].isDoubledDown ? 2 : 1)
|
||||
roundWinnings += handWinnings
|
||||
perHandWinnings.append(handWinnings)
|
||||
|
||||
if result == .blackjack {
|
||||
wasBlackjack = true
|
||||
@ -686,6 +691,24 @@ final class GameState {
|
||||
if result == .bust {
|
||||
hadBust = true
|
||||
}
|
||||
} else {
|
||||
perHandWinnings.append(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate insurance result
|
||||
var insResult: HandResult? = nil
|
||||
var insWinnings = 0
|
||||
if insuranceBet > 0 {
|
||||
if dealerHand.isBlackjack {
|
||||
insResult = .insuranceWin
|
||||
insWinnings = insuranceBet * 2 // Insurance pays 2:1
|
||||
balance += insuranceBet * 3 // Return bet + 2x winnings
|
||||
roundWinnings += insWinnings
|
||||
} else {
|
||||
insResult = .insuranceLose
|
||||
insWinnings = -insuranceBet // Lost insurance bet
|
||||
roundWinnings += insWinnings
|
||||
}
|
||||
}
|
||||
|
||||
@ -704,11 +727,13 @@ final class GameState {
|
||||
bustCount += 1
|
||||
}
|
||||
|
||||
// Create round result with all hand results
|
||||
// Create round result with all hand results and per-hand winnings
|
||||
let allHandResults = playerHands.map { $0.result ?? .lose }
|
||||
lastRoundResult = RoundResult(
|
||||
handResults: allHandResults,
|
||||
insuranceResult: insuranceBet > 0 ? (dealerHand.isBlackjack ? .insuranceWin : .insuranceLose) : nil,
|
||||
handWinnings: perHandWinnings,
|
||||
insuranceResult: insResult,
|
||||
insuranceWinnings: insWinnings,
|
||||
totalWinnings: roundWinnings,
|
||||
wasBlackjack: wasBlackjack
|
||||
)
|
||||
|
||||
@ -68,10 +68,34 @@ enum HandResult: Equatable {
|
||||
struct RoundResult: Equatable {
|
||||
/// Results for all player hands (index 0 = Hand 1, index 1 = Hand 2, etc.)
|
||||
let handResults: [HandResult]
|
||||
/// Net winnings for each hand (parallel to handResults)
|
||||
let handWinnings: [Int]
|
||||
let insuranceResult: HandResult?
|
||||
/// Insurance winnings (positive if won, negative if lost)
|
||||
let insuranceWinnings: Int
|
||||
let totalWinnings: Int
|
||||
let wasBlackjack: Bool
|
||||
|
||||
/// Convenience initializer without per-hand winnings (backwards compatibility)
|
||||
init(handResults: [HandResult], insuranceResult: HandResult?, totalWinnings: Int, wasBlackjack: Bool) {
|
||||
self.handResults = handResults
|
||||
self.handWinnings = [] // Empty means don't show per-hand amounts
|
||||
self.insuranceResult = insuranceResult
|
||||
self.insuranceWinnings = 0
|
||||
self.totalWinnings = totalWinnings
|
||||
self.wasBlackjack = wasBlackjack
|
||||
}
|
||||
|
||||
/// Full initializer with per-hand winnings
|
||||
init(handResults: [HandResult], handWinnings: [Int], insuranceResult: HandResult?, insuranceWinnings: Int, totalWinnings: Int, wasBlackjack: Bool) {
|
||||
self.handResults = handResults
|
||||
self.handWinnings = handWinnings
|
||||
self.insuranceResult = insuranceResult
|
||||
self.insuranceWinnings = insuranceWinnings
|
||||
self.totalWinnings = totalWinnings
|
||||
self.wasBlackjack = wasBlackjack
|
||||
}
|
||||
|
||||
/// The main/best result for display purposes (first hand, or best if split)
|
||||
var mainHandResult: HandResult {
|
||||
// Return the best result for the headline
|
||||
|
||||
@ -36,12 +36,16 @@ enum Design {
|
||||
|
||||
enum Size {
|
||||
// Hand scaling factor (1.5 = 50% larger hands)
|
||||
static let handScale: CGFloat = 1.5
|
||||
static let handScale: CGFloat = 1.75
|
||||
|
||||
// Cards - scaled for better visibility
|
||||
static let cardWidth: CGFloat = 60 * handScale // 90pt at 1.5x
|
||||
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
|
||||
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap
|
||||
|
||||
/// Card overlap (negative = cards stack left over right).
|
||||
/// More negative = more overlap (less card visible).
|
||||
/// With 90pt cards: -40 = ~44% overlap, -50 = ~55% overlap
|
||||
static let cardOverlap: CGFloat = -50
|
||||
|
||||
// Player hands container height (accommodates larger cards + labels)
|
||||
// Reduced from 180 to fit content more snugly
|
||||
|
||||
@ -72,10 +72,19 @@ struct GameTableView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func mainGameView(state: GameState) -> some View {
|
||||
ZStack {
|
||||
// Background
|
||||
TableBackgroundView()
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// Background
|
||||
TableBackgroundView()
|
||||
|
||||
mainContent(state: state, screenHeight: geometry.size.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func mainContent(state: GameState, screenHeight: CGFloat) -> some View {
|
||||
ZStack {
|
||||
VStack(spacing: 0) {
|
||||
// Top bar
|
||||
TopBarView(
|
||||
@ -110,7 +119,8 @@ struct GameTableView: View {
|
||||
// Table layout - fills available space
|
||||
BlackjackTableView(
|
||||
state: state,
|
||||
onPlaceBet: { placeBet(state: state) }
|
||||
onPlaceBet: { placeBet(state: state) },
|
||||
screenHeight: screenHeight
|
||||
)
|
||||
.frame(maxWidth: maxContentWidth)
|
||||
|
||||
|
||||
@ -68,19 +68,31 @@ struct ResultBannerView: View {
|
||||
.font(.system(size: amountFontSize, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(winningsColor)
|
||||
|
||||
// Breakdown - all hands
|
||||
// Breakdown - all hands with amounts for splits
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
ForEach(result.handResults.indices, id: \.self) { index in
|
||||
let handResult = result.handResults[index]
|
||||
let handWinnings = index < result.handWinnings.count ? result.handWinnings[index] : nil
|
||||
// Hand numbering: index 0 = Hand 1 (played first, displayed rightmost)
|
||||
let handLabel = result.handResults.count > 1
|
||||
? String(localized: "Hand \(index + 1)")
|
||||
: String(localized: "Main Hand")
|
||||
ResultRow(label: handLabel, result: handResult)
|
||||
// Show amounts for split hands, or for single hand if there are winnings
|
||||
let showAmount = result.hadSplit && handWinnings != nil
|
||||
ResultRow(
|
||||
label: handLabel,
|
||||
result: handResult,
|
||||
amount: showAmount ? handWinnings : nil
|
||||
)
|
||||
}
|
||||
|
||||
if let insuranceResult = result.insuranceResult {
|
||||
ResultRow(label: String(localized: "Insurance"), result: insuranceResult)
|
||||
let showInsAmount = result.insuranceWinnings != 0
|
||||
ResultRow(
|
||||
label: String(localized: "Insurance"),
|
||||
result: insuranceResult,
|
||||
amount: showInsAmount ? result.insuranceWinnings : nil
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
@ -189,6 +201,25 @@ struct ResultBannerView: View {
|
||||
struct ResultRow: View {
|
||||
let label: String
|
||||
let result: HandResult
|
||||
var amount: Int? = nil
|
||||
|
||||
private var amountText: String? {
|
||||
guard let amount = amount else { return nil }
|
||||
if amount > 0 {
|
||||
return "+$\(amount)"
|
||||
} else if amount < 0 {
|
||||
return "-$\(abs(amount))"
|
||||
} else {
|
||||
return "$0"
|
||||
}
|
||||
}
|
||||
|
||||
private var amountColor: Color {
|
||||
guard let amount = amount else { return .white }
|
||||
if amount > 0 { return .green }
|
||||
if amount < 0 { return .red }
|
||||
return .blue
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@ -198,9 +229,18 @@ struct ResultRow: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
// Show amount if provided
|
||||
if let amountText = amountText {
|
||||
Text(amountText)
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(amountColor)
|
||||
.frame(width: 70, alignment: .trailing)
|
||||
}
|
||||
|
||||
Text(result.displayText)
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .bold))
|
||||
.foregroundStyle(result.color)
|
||||
.frame(width: 100, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -209,7 +249,9 @@ struct ResultRow: View {
|
||||
ResultBannerView(
|
||||
result: RoundResult(
|
||||
handResults: [.blackjack],
|
||||
handWinnings: [150],
|
||||
insuranceResult: nil,
|
||||
insuranceWinnings: 0,
|
||||
totalWinnings: 150,
|
||||
wasBlackjack: true
|
||||
),
|
||||
@ -224,11 +266,30 @@ struct ResultRow: View {
|
||||
ResultBannerView(
|
||||
result: RoundResult(
|
||||
handResults: [.bust, .win, .push],
|
||||
handWinnings: [-100, 100, 0],
|
||||
insuranceResult: nil,
|
||||
totalWinnings: 25,
|
||||
insuranceWinnings: 0,
|
||||
totalWinnings: 0,
|
||||
wasBlackjack: false
|
||||
),
|
||||
currentBalance: 1025,
|
||||
currentBalance: 1000,
|
||||
minBet: 10,
|
||||
onNewRound: {},
|
||||
onPlayAgain: {}
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("Split with Insurance") {
|
||||
ResultBannerView(
|
||||
result: RoundResult(
|
||||
handResults: [.lose, .win],
|
||||
handWinnings: [-100, 200],
|
||||
insuranceResult: .insuranceWin,
|
||||
insuranceWinnings: 100,
|
||||
totalWinnings: 200,
|
||||
wasBlackjack: false
|
||||
),
|
||||
currentBalance: 1200,
|
||||
minBet: 10,
|
||||
onNewRound: {},
|
||||
onPlayAgain: {}
|
||||
|
||||
@ -12,6 +12,9 @@ struct BlackjackTableView: View {
|
||||
@Bindable var state: GameState
|
||||
let onPlaceBet: () -> Void
|
||||
|
||||
/// Screen height passed from parent for responsive sizing
|
||||
var screenHeight: CGFloat = 800
|
||||
|
||||
/// Whether to show Hi-Lo card count values on cards.
|
||||
var showCardCount: Bool { state.settings.showCardCount }
|
||||
|
||||
@ -32,6 +35,23 @@ struct BlackjackTableView: View {
|
||||
// Use global debug flag from Design constants
|
||||
private var showDebugBorders: Bool { Design.showDebugBorders }
|
||||
|
||||
/// Dynamic spacer height based on screen size.
|
||||
/// Formula: spacing = clamp((screenHeight - baseline) * scale, min, max)
|
||||
/// This produces smooth scaling across all device sizes:
|
||||
/// - iPhone SE (~667pt): ~20pt
|
||||
/// - iPhone Pro Max (~932pt): ~76pt
|
||||
/// - iPad Mini (~1024pt): ~95pt
|
||||
/// - iPad Pro 12.9" (~1366pt): ~150pt (capped)
|
||||
private var dealerPlayerSpacing: CGFloat {
|
||||
let baseline: CGFloat = 550 // Below this, use minimum
|
||||
let scale: CGFloat = 0.2 // 20% of height above baseline
|
||||
let minSpacing: CGFloat = 20 // Floor for smallest screens
|
||||
let maxSpacing: CGFloat = 150 // Ceiling for largest screens
|
||||
|
||||
let calculated = (screenHeight - baseline) * scale
|
||||
return min(maxSpacing, max(minSpacing, calculated))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
// Dealer area
|
||||
@ -44,9 +64,9 @@ struct BlackjackTableView: View {
|
||||
)
|
||||
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
|
||||
|
||||
// Flexible space between dealer and player (minimum 60pt)
|
||||
Spacer(minLength: 60)
|
||||
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer")
|
||||
// Flexible space between dealer and player - scales with screen size
|
||||
Spacer(minLength: dealerPlayerSpacing)
|
||||
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer \(Int(dealerPlayerSpacing))")
|
||||
|
||||
// Player hands area - only show when there are cards dealt
|
||||
if state.playerHands.first?.cards.isEmpty == false {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user