UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 25: feedback for Rock Paper Scissors Brain Training app

Forums > 100 Days of SwiftUI

Hi!

I've just finished the Rock Paper Scissors challenge. I feel like I did fairly well, but I would love some feedback to see where I can improve.

The most difficult thing for me in this challenge was refactoring my checkAnswer function. I have tons of if else statements in there and that is after already doing some refactoring. I struggle with finding a better way then using if else statements from time to time. I considered a Switch here but looking through Swifts documentation, I decided that it wouldn't work in this case as far as I could see.

Here's my code, along with a screenshot of the end result:

import SwiftUI

struct ContentView: View {

    @State private var possibleAnswers = ["Rock", "Paper", "Scissors"] // The possible options the app/computer opponent can choose
    @State private var computerPick = Int.random(in:0...2) // Used to randomize appChoice
    @State private var mustWin = Bool.random() // Decides whether the user should win or lose

    @State private var questionCounter = 1 // Tracks how many questions there've been
    @State private var userScore = 0 // Tracks the users score

    @State private var scoreAlert = "" // This will be displayed in an alert when an answer is selected and is set in the answerSelected function

    @State private var showScore = false // Used to trigger alert after an answer is selected
    @State private var gameOver = false // Used to trigger an alert after the final question

    var body: some View {
        ZStack {
            RadialGradient(stops: [
                .init(color: Color.indigo, location: 0.3),
                .init(color: Color.mint, location: 0.3)
            ], center: .top, startRadius: 200, endRadius: 300)
            .ignoresSafeArea()
            VStack {
                Text("Brain Training")
                    .font(.largeTitle).bold()
                    .foregroundColor(.white)
                    .frame(height: 100)
                Text("Question \(questionCounter) of 10")
                    .font(.headline.bold())
                    .foregroundColor(.white)
                Spacer()
                Text("Score: \(userScore)")
                    .font(.largeTitle).bold()
                    .foregroundColor(.white)
            }

            VStack {
                Spacer()
                Spacer()
                Text(mustWin ? "You must win!" : "You must lose!"  ) // Ternary operator that sets the text based on if mustWin is true or false
                    .frame(height: 70)
                    .font(.largeTitle.bold().smallCaps())
                    .foregroundColor(.indigo)
                    .background(.thinMaterial)
                    .padding()

                Text("The computer chose: ")
                    .frame(height: 40)
                    .font(.title.bold().smallCaps())
                    .foregroundColor(.white)
                    .background(.ultraThinMaterial)
                Image("\(possibleAnswers[computerPick])")
                    .resizable()
                    .frame(width: 100, height: 100)
                Text("Choose your move:")
                    .font(.title.bold().smallCaps())
                    .foregroundColor(.white)
                    .background(.ultraThinMaterial)

                HStack {
                    ForEach(0..<3) { number in // Used to loop through possibleAnswers array and display images
                        Button {
                            checkAnswer(number)
                        } label: {
                            Image("\(possibleAnswers[number])")
                                .resizable()
                                .frame(width: 100, height: 100)
                        }
                    }
                }
                Spacer()
            }
        }
        .alert(scoreAlert, isPresented: $showScore) {
            Button("Next question", action: newQuestion)
        } message: {
            Text("Your score is \(userScore)")
        }

        .alert("Game finished!", isPresented: $gameOver) {
            Button("Restart game", action: restartGame)
        } message: {
            Text("You finished the game with a score of \(userScore).")
        }
    }

    func checkAnswer(_ userChoice: Int) {
        let requirementWinLose = mustWin ? "win" : "lose"
        let correctAnswerAlert = "Correct! You had to \(requirementWinLose). You chose \(possibleAnswers[userChoice]) and the computer chose \(possibleAnswers[computerPick])"
        let incorrectAnswerAlert = "Incorrect! You had to \(requirementWinLose). You chose \(possibleAnswers[userChoice]) and the computer chose \(possibleAnswers[computerPick])"

        if mustWin {
            if userChoice == 0 && computerPick == 0 || userChoice == 0 && computerPick == 1 {
                scoreAlert = incorrectAnswerAlert
                userScore-=1
            } else if userChoice == 0 && computerPick == 2 {
                scoreAlert = correctAnswerAlert
                userScore+=1
            } else if userChoice == 1 && computerPick == 1 || userChoice == 1 && computerPick == 2 {
                scoreAlert = incorrectAnswerAlert
                userScore-=1
            } else if userChoice == 1 && computerPick == 0 {
                scoreAlert = correctAnswerAlert
                userScore+=1
            } else if userChoice == 2 && computerPick == 0 || userChoice == 2 && computerPick == 2 {
                scoreAlert = incorrectAnswerAlert
                userScore-=1
            } else if userChoice == 2 && computerPick == 1 {
                scoreAlert = correctAnswerAlert
                userScore+=1
            }
        } else {
            if userChoice == 0 && computerPick == 0 || userChoice == 0 && computerPick == 2 {
                scoreAlert = incorrectAnswerAlert
                userScore-=1
            } else if userChoice == 0 && computerPick == 1 {
                scoreAlert = correctAnswerAlert
                userScore+=1
            } else if userChoice == 1 && computerPick == 0 || userChoice == 1 && computerPick == 1 {
                scoreAlert = incorrectAnswerAlert
                userScore-=1
            } else if userChoice == 1 && computerPick == 2 {
                scoreAlert = correctAnswerAlert
                userScore+=1
            } else if userChoice == 2 && computerPick == 1 || userChoice == 2 && computerPick == 2 {
                scoreAlert = incorrectAnswerAlert
                userScore-=1
            } else if userChoice == 2 && computerPick == 0 {
                scoreAlert = correctAnswerAlert
                userScore+=1
            }
        }

        showScore = true
        trackQuestions()

    }

    func trackQuestions() {
        if questionCounter == 10 {
            gameOver = true
            showScore = false
        }
    }

    func newQuestion() {
        computerPick = Int.random(in: 0...2)
        mustWin.toggle()
        questionCounter+=1
    }

    func restartGame() {
        computerPick = Int.random(in: 0...2)
        mustWin.toggle()
        userScore = 0
        questionCounter = 1
    }

}

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

Thank you for your time!

1      

Darrell asks for feedback, but wisely doesn't ask for answers!

I've just finished the Rock Paper Scissors challenge.
I feel like I did fairly well, but I would love some feedback to see where I can improve.

You used strings to represent the three options in your application. What syntax allows you to limit options to only predetermined choices? How can you leverage this to encapsulate game logic into usable methods?

See -> Day 3: Complex Data Structures

Here's a short bit I wrote for another programmer. It includes hints that may help guide you.

See -> Rock, Paper, Scissors Help

2      

Darrell asks for feedback, but wisely doesn't ask for answers!

... but I would love some feedback to see where I can improve.

This is one of my peeves; others disagree.

ContentView

In decades of coding courses and C+ Programming for Dummies type books, function names were often given the generic name Foo or perhaps Bar. These were nonsense names to help the reader focus on syntax, or on a technique, and not necessarily what the function was designed to do.

When you create a new SwiftUI view, XCode gives the view a nonsense name: ContentView! Why not BoxView? or FooView? Ugh.

@twoStraws and most all other SwiftUI content providers in YouTube land will express the need for descriptive variable names. They'll note how important it is to use camelCase, or to capitalize the names of Structs and Classes. Indeed, when creating other views in a project, they're very careful to give the new views a meaningful name!

Yet, they'll often leave the first view named ContentView.

Well, you asked for feedback and ways to improve. Here's my personal feedback. As soon as you create a new SwiftUI View, change the name from ContentView to something meaningful in your application.

For example, in your game, the main focus of your applicaiton is the GameView.

Factor UI into Smaller Chunks

Here's another place where you can start to improve your code.
This code is easy, but takes up 13 lines in your GameView. Consider extracting this from your GameView and putting it into its own view named HeaderView

VStack {
    Text("Brain Training")
      .font(.largeTitle).bold()
      .foregroundColor(.white)
      .frame(height: 100)
    Text("Question \(questionCounter) of 10")
      .font(.headline.bold())
      .foregroundColor(.white)
    Spacer()
    Text("Score: \(userScore)")
      .font(.largeTitle).bold()
      .foregroundColor(.white)
}

Reusable Lego Bricks

I think of this as building your own Lego pieces. After building a few, you can snap your GameView together with your custom Lego pieces. Down the road, you might think about reusing a custom Lego piece from an older application in your newer one. Reusable code? Nice!

Then in your GameView, you can call:

// Reusable Lego brick
HeaderView(showingQuestion: questionCounter, of: questionCount, withScore: userScore)

This cleans up your GameView and helps you separate interface elements into bite-size chunks.

2      

Hi Obelix!

Thank you so much for your great advice. I will try to implement it into the project in the coming days and report back here again. :)

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.