logo
Updated

iOS: Refactor app (part1)

User: Ok, so this is what we got to.


//
//  ContentView.swift
//  BlackJackPractice
//
//  Created by Victor Quach on 3/22/23.
//

import SwiftUI


struct SplitsView: View {
    @State private var playerCard1: Int = 11
    @State private var playerCard2: Int = 11
    @State private var dealerCard: Int = 7
    @State private var feedbackMessage: String = ""
    
    func cardName(_ card: Int) -> String {
            switch card {
            case 1:
                return "A"
            case 10:
                return "T"
            case 11:
                return "J"
            case 12:
                return "Q"
            case 13:
                return "K"
            default:
                return String(card)
            }
    }
    
    
    func randomCard() -> Int {
        let cardValues = [1,2,3,4,5,6,7,8,9,10,11,12,13]
        return cardValues.randomElement()!
    }

    func shouldSplitCards(_ playerCard1: Int, _ playerCard2: Int, _ dealerCard: Int) -> (Bool, String) {
        assert(playerCard1 == playerCard2, "The player cards should be the same");
        // Always split aces and 8s
        if playerCard1 == 1 || playerCard1 == 8 {
            return (true, "Always split aces and 8s.")
        }
        // Never split 10s and 5s
        if playerCard1 == 5 || playerCard2 >= 10 {
            return (false, "Never split 10s and 5s.")
        }
        // Split 2s, 3s, 7s against 2-7
        if playerCard1 == 2 || playerCard1 == 3 || playerCard1 == 7 {
            if dealerCard >= 2 && dealerCard <= 7 {
                return (true, "Split 2s, 3s, 7s against 2-7.")
            } else {
                return (false, "Split 2s, 3s, 7s against 2-7.")
            }
        }
        // Split 4s against 5 or 6
        if playerCard1 == 4 {
            if dealerCard == 5 || dealerCard == 6 {
                return (true, "Split 4s against 5 or 6.")
            } else {
                return (false, "Split 4s against 5 or 6.")
            }
        }
        // Split 6s against 2-6
        if playerCard1 == 6 {
            if dealerCard >= 2 && dealerCard <= 6 {
                return (true, "Split 6s against 2-6.")
            } else {
                return (false, "Split 6s against 2-6.")
            }
        }
        // Split 9s against 2-6 and 8-9
        if playerCard1 == 9 {
            if dealerCard >= 2 && dealerCard <= 6 || dealerCard >= 8 && dealerCard <= 9 {
                return (true, "Split 9s against 2-6 and 8-9.")
            } else {
                return (false, "Split 9s against 2-6 and 8-9.")
            }
        }
        
        return (false, "No specific rule for this situation.")
    }
    
    var body: some View {
        VStack(spacing: 16) {
            Text(feedbackMessage)
            Text("Dealer: \(cardName(dealerCard))")
            Text("You: \(cardName(playerCard1))\(cardName(playerCard2))")
            
            HStack {
                Button(action: {
                    let (shouldSplit, reason) = shouldSplitCards(playerCard1, playerCard2, dealerCard)
                    feedbackMessage = shouldSplit ? "Correct! \(reason)" : "Incorrect. \(reason)"
                }) {
                    Text("Split")
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
                Button(action: {
                    let (shouldSplit, reason) = shouldSplitCards(playerCard1, playerCard2, dealerCard)
                    feedbackMessage = !shouldSplit ? "Correct! \(reason)" : "Incorrect. \(reason)"
                    
                }) {
                    Text("Don't split")
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
            }
            Button(action: {
                // Generate new random cards for the player and dealer
                playerCard1 = randomCard()
                playerCard2 = playerCard1
                dealerCard = randomCard()

                // Clear the feedback message
                feedbackMessage = ""
            }) {
                Text("New Cards")
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
}

struct SoftCountsView: View {
    @State private var playerCard1: Int = 1
    @State private var playerCard2: Int = 5
    @State private var dealerCard: Int = 7
    @State private var feedbackMessage: String = ""
    
    enum Action: String {
        case stand = "STAND"
        case hit = "HIT"
        case double = "DOUBLE"
        case split = "SPLIT"
    }
    
    func cardName(_ card: Int) -> String {
            switch card {
            case 1:
                return "A"
            case 10:
                return "T"
            case 11:
                return "J"
            case 12:
                return "Q"
            case 13:
                return "K"
            default:
                return String(card)
            }
    }
    
    
    func randomCard() -> Int {
        let cardValues = [1,2,3,4,5,6,7,8,9,10,11,12,13]
        return cardValues.randomElement()!
    }
    
    func getActionOnSoftCount(playerCard1: Int, playerCard2: Int, dealerCard: Int) -> (action: Action, message: String) {
        assert(playerCard1 == 1 || playerCard2 == 1)
        if playerCard1 != 1 {
            return getActionOnSoftCount(playerCard1: playerCard2, playerCard2: playerCard1, dealerCard: dealerCard)
        }

        if playerCard2 == 1 {
            return (action: .split, message: "Always split aces")
        }
        if playerCard2 == 2 || playerCard2 == 3 {
            if dealerCard >= 5 && dealerCard <= 6 {
                return (action: .double, message: "Double soft 13-14 against 5-6, otw hit")
            } else {
                return (action: .hit, message: "Double soft 13-14 against 5-6, otw hit")
            }
        }
        if playerCard2 == 4 || playerCard2 == 5 {
            if dealerCard >= 4 && dealerCard <= 6 {
                return (action: .double, message: "Double soft 15-16 against 4-6, otw hit")
            } else {
                return (action: .hit, message: "Double soft 15-16 against 4-6, otw hit")
            }
        }
        if playerCard2 == 6 {
            if dealerCard >= 3 && dealerCard <= 6 {
                return (action: .double, message: "Double soft 17 against 3-6, otw hit")
            } else {
                return (action: .hit, message: "Double soft 17 against 3-6, otw hit")
            }
        }
        if playerCard2 == 7 {
            if dealerCard >= 2 && dealerCard <= 6 {
                return (action: .double, message: "Soft 18: double against 2-6, stands against 7-8, hits against 9, 10, Ace. If it can't double against 2-6, it stands")
            } else if dealerCard == 7 || dealerCard == 8 {
                return (action: .stand, message: "Soft 18: double against 2-6, stands against 7-8, hits against 9, 10, Ace. If it can't double against 2-6, it stands")
            } else {
                return (action: .hit, message: "Soft 18: double against 2-6, stands against 7-8, hits against 9, 10, Ace. If it can't double against 2-6, it stands")
            }
        }
        if playerCard2 == 8 {
            if dealerCard == 6 {
                return (action: .double, message: "Soft 19: double against 6, otw stand")
            } else {
                return (action: .stand, message: "Soft 19: double against 6, otw stand")
            }
        }
        if playerCard2 >= 9 {
            return (action: .stand, message: "Soft 20-21: stand")
        }

        return (action: .hit, message: "Unknown")
    }

    
    var body: some View {
        VStack(spacing: 16) {
            Text(feedbackMessage)
            Text("Dealer: \(cardName(dealerCard))")
            Text("You: \(cardName(playerCard1))\(cardName(playerCard2))")
            
            VStack(spacing: 16) {
                
                HStack(spacing: 16) {
                    Button(action: { // Stand
                        let (action, reason) = getActionOnSoftCount(playerCard1: playerCard1, playerCard2: playerCard2, dealerCard: dealerCard);
                        feedbackMessage = action == Action.stand ? "Correct! \(reason)" : "Incorrect. \(reason)"
                    }) {
                        Text("Stand")
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                    Button(action: { // Hit
                        let (action, reason) = getActionOnSoftCount(playerCard1: playerCard1, playerCard2: playerCard2, dealerCard: dealerCard);
                        feedbackMessage = action == Action.hit ? "Correct! \(reason)" : "Incorrect. \(reason)"
                    }) {
                        Text("Hit")
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                }
                HStack(spacing: 16) {
                    Button(action: { // Split
                        let (action, reason) = getActionOnSoftCount(playerCard1: playerCard1, playerCard2: playerCard2, dealerCard: dealerCard);
                        feedbackMessage = action == Action.split ? "Correct! \(reason)" : "Incorrect. \(reason)"
                    }) {
                        Text("Split")
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                    Button(action: { // Double
                        let (action, reason) = getActionOnSoftCount(playerCard1: playerCard1, playerCard2: playerCard2, dealerCard: dealerCard);
                        feedbackMessage = action == Action.double ? "Correct! \(reason)" : "Incorrect. \(reason)"
                    }) {
                        Text("Double")
                            .padding()
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                }
                
                }
            Button(action: {
                // Generate new random cards for the player and dealer
                playerCard1 = 1
                playerCard2 = randomCard()
                dealerCard = randomCard()

                // Clear the feedback message
                feedbackMessage = ""
            }) {
                Text("New Cards")
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
}

struct HardCountsView: View {
    var body: some View {
        Text("HardCounts")
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            MenuView()
        }
    }
}

struct MenuView: View {
    let buttonWidth: CGFloat = 200

    var body: some View {
        VStack(spacing: 16) {
            NavigationLink(destination: SplitsView()) {
                Text("Splits")
                    .padding()
                    .frame(width: buttonWidth)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }

            NavigationLink(destination: SoftCountsView()) {
                Text("Soft Counts")
                    .padding()
                    .frame(width: buttonWidth)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }

            NavigationLink(destination: HardCountsView()) {
                Text("Hard Counts")
                    .padding()
                    .frame(width: buttonWidth)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Seems like we should refactor this.

To refactor the code and make it more modular, you can create separate custom view components for the cards, buttons, and other elements. Here's a proposed continuation:

Create a CardView component to display the cards:

struct CardView: View {
    let card: Int

    func cardName(_ card: Int) -> String {
        switch card {
        case 1:
            return "A"
        case 10:
            return "T"
        case 11:
            return "J"
        case 12:
            return "Q"
        case 13:
            return "K"
        default:
            return String(card)
        }
    }

    var body: some View {
        Text(cardName(card))
    }
}

Create a ActionButton component for the action buttons:

struct ActionButton: View {
    let title: String
    let backgroundColor: Color
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Text(title)
                .padding()
                .background(backgroundColor)
                .foregroundColor(.white)
                .cornerRadius(8)
        }
    }
}

Replace the card display and buttons in SplitsView and SoftCountsView with the new components:

In SplitsView:

Text("Dealer: ")
    + CardView(card: dealerCard)
Text("You: ")
    + CardView(card: playerCard1)
    + CardView(card: playerCard2)

// ...

ActionButton(title: "Split", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Don't split", backgroundColor: Color.red, action: {
    // ...
})
ActionButton(title: "New Cards", backgroundColor: Color.green, action: {
    // ...
})

In SoftCountsView:

Text("Dealer: ")
    + CardView(card: dealerCard)
Text("You: ")
    + CardView(card: playerCard1)
    + CardView(card: playerCard2)

// ...

ActionButton(title: "Stand", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Hit", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Split", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Double", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "New Cards", backgroundColor: Color.green, action: {
    // ...
})

This will make the code more readable and maintainable by separating the components and making them reusable.

User: Maybe we can refactor the views as well. There are repeated functions like randomCard

To refactor the views and eliminate the repeated randomCard function, you can create a separate utility file to store common functions.

Create a new Swift file named Utility.swift:

import Foundation

class Utility {
    static func randomCard() -> Int {
        let cardValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
        return cardValues.randomElement()!
    }
}

Now, you can replace the randomCard function calls in both SplitsView and SoftCountsView with Utility.randomCard():

In SplitsView:

Button(action: {
    // Generate new random cards for the player and dealer
    playerCard1 = Utility.randomCard()
    playerCard2 = playerCard1
    dealerCard = Utility.randomCard()

    // Clear the feedback message
    feedbackMessage = ""
}) {
    Text("New Cards")
        .padding()
        .background(Color.green)
        .foregroundColor(.white)
        .cornerRadius(8)
}

In SoftCountsView:

Button(action: {
    // Generate new random cards for the player and dealer
    playerCard1 = 1
    playerCard2 = Utility.randomCard()
    dealerCard = Utility.randomCard()

    // Clear the feedback message
    feedbackMessage = ""
}) {
    Text("New Cards")
        .padding()
        .background(Color.green)
        .foregroundColor(.white)
        .cornerRadius(8)
}

This refactoring will make the code more organized and maintainable by centralizing the common functions in a utility class.

User: Wait a minute, I got the error: Cannot convert value of type 'CardView' to expected argument type 'Text'

Apologies for the confusion. To fix the error, you can use HStack to properly display the CardView components alongside the Text components. Here's the updated code for SplitsView and SoftCountsView:

In SplitsView:

HStack {
    Text("Dealer: ")
    CardView(card: dealerCard)
}
HStack {
    Text("You: ")
    CardView(card: playerCard1)
    CardView(card: playerCard2)
}

// ...

ActionButton(title: "Split", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Don't split", backgroundColor: Color.red, action: {
    // ...
})
ActionButton(title: "New Cards", backgroundColor: Color.green, action: {
    // ...
})

In SoftCountsView:

HStack {
    Text("Dealer: ")
    CardView(card: dealerCard)
}
HStack {
    Text("You: ")
    CardView(card: playerCard1)
    CardView(card: playerCard2)
}

// ...

ActionButton(title: "Stand", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Hit", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Split", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "Double", backgroundColor: Color.blue, action: {
    // ...
})
ActionButton(title: "New Cards", backgroundColor: Color.green, action: {
    // ...
})

By using HStack, you can display the CardView components horizontally alongside the Text components without any errors.

User: How do I use CardView with constants? For instance CardView(card: 12) does not work.
For example, the code


//
//  CardView.swift
//  BlackJackPractice
//
//  Created by Victor Quach on 3/22/23.
//

import SwiftUI

struct CardView: View {
    let card: Int
    
    init(card: Int) {
        self.card = card
    }

    func cardName(_ card: Int) -> String {
        switch card {
        case 1:
            return "A"
        case 10:
            return "T"
        case 11:
            return "J"
        case 12:
            return "Q"
        case 13:
            return "K"
        default:
            return String(card)
        }
    }

    var body: some View {
        Text(cardName(card))
    }
}


struct CardView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            CardView(card: 1)
            CardView(card: 2)
            CardView(card: 3)
            CardView(card: 4)
            CardView(card: 5)
            CardView(card: 6)
            CardView(card: 7)
            CardView(card: 8)
            CardView(card: 9)
            CardView(card: 10)
            CardView(card: 11)
            CardView(card: 12)
            CardView(card: 13)
        }
    }
}

fails with


extra arguments at positions #11, #12, #13 in call

----------------------------------------

CompileDylibError: Failed to build CardView.swift

Compiling failed: extra arguments at positions #11, #12, #13 in call

/Users/varal7/Projects/BlackJackPractice/BlackJackPractice/BlackJackPractice/CardView.swift:43:13: error: extra arguments at positions #11, #12, #13 in call
            CardView(card: __designTimeInteger("#13530.[2].[0].property.[0].[0].arg[0].value.[0].arg[0].value", fallback: 1))
            ^
SwiftUI.ViewBuilder:3:24: note: 'buildBlock' declared here
    public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
                       ^

What to do

The error occurs because the SwiftUI.ViewBuilder's buildBlock function supports up to 10 view elements. Since you have 13 CardView instances inside the VStack, it exceeds the limit.

To fix this issue, you can group CardView instances into multiple VStacks or HStacks and then combine them. Here's a modified version of the CardView_Previews struct:

struct CardView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            HStack {
                CardView(card: 1)
                CardView(card: 2)
                CardView(card: 3)
                CardView(card: 4)
                CardView(card: 5)
            }
            HStack {
                CardView(card: 6)
                CardView(card: 7)
                CardView(card: 8)
                CardView(card: 9)
                CardView(card: 10)
            }
            HStack {
                CardView(card: 11)
                CardView(card: 12)
                CardView(card: 13)
            }
        }
    }
}

This change will resolve the error and display all CardView instances as expected.