From 03ee03b8761a4690dbf3146b089787fb6eb665f4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 22 Dec 2025 12:20:04 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- Blackjack/Agents.md | 60 +++++++++++++++++++- Blackjack/Engine/GameState.swift | 15 +++++ Blackjack/Views/Game/ActionButtonsView.swift | 14 +---- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/Blackjack/Agents.md b/Blackjack/Agents.md index f4bf715..4bc821c 100644 --- a/Blackjack/Agents.md +++ b/Blackjack/Agents.md @@ -49,8 +49,66 @@ You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and relat - Do not use `GeometryReader` if a newer alternative would work as well, such as `containerRelativeFrame()` or `visualEffect()`. - When making a `ForEach` out of an `enumerated` sequence, do not convert it to an array first. So, prefer `ForEach(x.enumerated(), id: \.element.id)` instead of `ForEach(Array(x.enumerated()), id: \.element.id)`. - When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer. -- Place view logic into view models or similar, so it can be tested. - Avoid `AnyView` unless it is absolutely required. + + +## View/State separation (MVVM-lite) + +**Views should be "dumb" renderers.** All business logic belongs in `GameState` or dedicated view models. + +### What belongs in the State/ViewModel: +- **Business logic**: Calculations, validations, game rules +- **Computed properties based on game data**: hints, recommendations, derived values +- **State checks**: `isPlayerTurn`, `canHit`, `isGameOver`, `isBetBelowMinimum` +- **Data transformations**: statistics calculations, filtering, aggregations + +### What is acceptable in Views: +- **Pure UI layout logic**: `isIPad`, `maxContentWidth` based on size class +- **Visual styling**: color selection based on state (`valueColor`, `resultColor`) +- **@ViewBuilder sub-views**: breaking up complex layouts +- **Accessibility labels**: combining data into accessible descriptions + +### Examples + +**❌ BAD - Business logic in view:** +```swift +struct MyView: View { + @Bindable var state: GameState + + private var isBetBelowMinimum: Bool { + state.currentBet > 0 && state.currentBet < state.settings.minBet + } + + private var currentHint: String? { + guard let hand = state.activeHand else { return nil } + return state.engine.getHint(playerHand: hand, dealerUpCard: upCard) + } +} +``` + +**✅ GOOD - Logic in GameState, view just reads:** +```swift +// In GameState: +var isBetBelowMinimum: Bool { + currentBet > 0 && currentBet < settings.minBet +} + +var currentHint: String? { + guard settings.showHints, isPlayerTurn else { return nil } + guard let hand = activeHand, let upCard = dealerUpCard else { return nil } + return engine.getHint(playerHand: hand, dealerUpCard: upCard) +} + +// In View: +if state.isBetBelowMinimum { ... } +if let hint = state.currentHint { HintView(hint: hint) } +``` + +### Benefits: +- **Testable**: GameState logic can be unit tested without UI +- **Single source of truth**: No duplicated logic across views +- **Cleaner views**: Views focus purely on layout and presentation +- **Easier debugging**: Logic is centralized, not scattered - **Never use raw numeric literals** for padding, spacing, opacity, font sizes, dimensions, corner radii, shadows, or animation durations—always use Design constants (see "No magic numbers" section). - **Never use inline `Color(red:green:blue:)` or hex colors**—define all colors in the `Color` extension in `DesignConstants.swift` with semantic names. - Avoid using UIKit colors in SwiftUI code. diff --git a/Blackjack/Engine/GameState.swift b/Blackjack/Engine/GameState.swift index c6341cb..f485c38 100644 --- a/Blackjack/Engine/GameState.swift +++ b/Blackjack/Engine/GameState.swift @@ -185,6 +185,21 @@ final class GameState { return engine.getHint(playerHand: hand, dealerUpCard: upCard) } + /// Whether the current bet is below the minimum required. + var isBetBelowMinimum: Bool { + currentBet > 0 && currentBet < settings.minBet + } + + /// Amount needed to reach minimum bet. + var amountNeededForMinimum: Int { + max(0, settings.minBet - currentBet) + } + + /// Whether the current bet has reached the maximum. + var isBetAtMaximum: Bool { + currentBet >= settings.maxBet + } + /// Betting recommendation based on the true count. var bettingHint: String? { guard settings.showCardCount else { return nil } diff --git a/Blackjack/Views/Game/ActionButtonsView.swift b/Blackjack/Views/Game/ActionButtonsView.swift index 6fdcb1c..759055a 100644 --- a/Blackjack/Views/Game/ActionButtonsView.swift +++ b/Blackjack/Views/Game/ActionButtonsView.swift @@ -43,23 +43,13 @@ struct ActionButtonsView: View { // MARK: - Betting Phase Buttons - /// Whether the current bet meets the minimum requirement - private var isBetBelowMinimum: Bool { - state.currentBet > 0 && state.currentBet < state.settings.minBet - } - - /// Amount needed to reach minimum bet - private var amountNeededForMinimum: Int { - state.settings.minBet - state.currentBet - } - @ViewBuilder private var bettingButtons: some View { if state.currentBet > 0 { VStack(spacing: Design.Spacing.small) { // Show hint if bet is below minimum - if isBetBelowMinimum { - Text(String(localized: "Add $\(amountNeededForMinimum) more to meet minimum")) + if state.isBetBelowMinimum { + Text(String(localized: "Add $\(state.amountNeededForMinimum) more to meet minimum")) .font(.system(size: Design.BaseFontSize.small, weight: .medium)) .foregroundStyle(.orange) .transition(.opacity)