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

Rock Paper Scissors Day 25 Challenge

Forums > 100 Days of SwiftUI

Hi all

Im loving the 100 days of swiftui course. im on day 25 now.

i worked hard on this challenge and wondered if i could have some feedback please on my code. ive not gone for fancy pics and backgrounds with this, just wanted to get the logic and code practice right. somit doesnt look flashy but it works. I originally coded seperste views for the computer choice but found that any @State variables I placced in the view struct would not be available to use in the other structs. i couldnt work out how to set them so they were available everywhere. Any tips would be appreciated.

im using swift 4 playgrounds on ipad at the mo.

thank you!

layth

import SwiftUI

struct ContentView: View {

    @State private var options = ["🪨", "🗒","✂️"] // rock paper scissors!
    @State private var needToWin = Bool.random() // Returns true or false about whether the player needs to win
    @State private var rounds = 0 // number of game play rounds 
    let computerNo = Int.random(in: 0...2) // chooses a random number between 0 and 2 that will later be the computers game move
    var toWin: String {
        if options[computerNo] == "🪨" {
            return "🗒"
        } else if options[computerNo] == "🗒" {
            return "✂️"
        } else {
            return "🪨"
        } // returns the winning answer
    }

    @State private var alertPresented = false // used to show the alert
    @State private var outcomeTitle = "" // will become the alert title
    @State private var wasCorrect = false // used to change the message in the alert
    @State private var score = 0 // tracks the score
    @State private(set) var highScore = 0 // records the high score
    @State private var hasEnded = false // used as a trigger after the last round

    var body: some View {
        ZStack{
            Color.gray
            VStack {
                Spacer()
                Text("Rock Paper Scissors")
                    .padding()
                    .scaledToFit()
                    .font(.largeTitle)
                Text("The computer chose...")
                    .padding()
                Text(options[computerNo])
                    .font(.largeTitle)
                Text("You must...")
                    .padding()
                Text(needToWin ? "Win" : "Lose")
                    .foregroundColor(needToWin ? .green : .red)
                    .padding()
                    .font(.largeTitle)
                Text("Choose wisely...")
                    .padding(30)
                HStack {
                    Spacer()
                    Button("🪨") {
                        let userOption = "🪨"
                        chosen(user: userOption)
                    } .foregroundColor(.yellow)
                        .font(.largeTitle)
                    Spacer()
                    Button("🗒") {
                        let userOption = "🗒"
                        chosen(user: userOption)
                    }.foregroundColor(.black)
                        .font(.largeTitle)
                    Spacer()
                    Button("✂️") {
                        let userOption = "✂️"
                        chosen(user: userOption)
                    } .foregroundColor(.indigo)
                        .font(.largeTitle)
                    Spacer()

                }
                Spacer()
                HStack{
                Text("Score: \(score)")
                    .padding(20)
                    VStack{
                        Text("High Score: \(highScore)")
                        Button("Reset") {
                            highScore = 0
                        }.foregroundColor(.yellow)
                    }
                }
            } .alert(outcomeTitle, isPresented: $alertPresented) {
                Button("Next", action: nextQuestion)
            } message: {
                if wasCorrect == true {
                    Text("Your score is \(score)")
                } else {
                    Text("Try again")
                }
            } .alert("Game Over", isPresented: $hasEnded) {
                Button("Restart game", action: gameOver)
            } message: {
                if wasCorrect == true {
                    Text("Correct! Your final score was \(score)")
                } else {
                    Text("Wrong! Your final score was \(score)")
                }
            }
        }
    }

    func chosen(user: String) {
        rounds += 1
        if user == toWin && needToWin == true {
            outcomeTitle = "Correct!"
            wasCorrect = true
            score += 1
        }
        if user == toWin && needToWin == false {
            outcomeTitle = "Wrong!"
            wasCorrect = false
        }
        if user != toWin && needToWin == true {
            outcomeTitle = "Wrong!"
            wasCorrect = false
        }
        if user != toWin && needToWin == false {
            outcomeTitle = "Correct!"
            wasCorrect = true
            score += 1
        }
        if rounds == 10 {
            hasEnded = true
        } else {
            alertPresented = true
        }

    }
    func nextQuestion() {
        options.shuffle()
        needToWin = Bool.random()
    }
    func gameOver() {
        nextQuestion()
        rounds = 0
        if score > highScore {
            highScore = score
        }
        score = 0
    }
}

2      

When you reach project 7 you will begin to learn how to do what you are asking about here. I would recommend just getting the project to work, and continue going through the course material. But if you want to jump ahead and try to learn about those things now, you can follow this link

When I first started trying to work through the 100 days course, I spent many hours trying to make extra features work on some of the projects, and I often found that if I had not wasted my time on trying to mess around with the code, I would have learned what I wanted to do in a fraction of the time by just continuing on to the next project where I would have had it explained to me in a simple way.

3      

@State private(set) var highScore = 0 // records the high score

The private(set) annotation on this line isn't really going to do anything useful for you.

private(set) means that anyone outside of the struct/class where this property is declared will only have read-only access to the property. But SwiftUI Views don't have a lifetime like regular structs/classes do. They are created and destroyed as needed by the view renderer and so you can't guarantee the View will even be available when you try to read this property.

What are you trying to accomplish by using private(set) here?

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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

thanks @Fly0strich - i did wonder if all would become clear and youre spot on ive been messing around trying to add stuff. fun though!

@Roosterboy - i was just messing around really - i thiught that by setting it like that it protected it from being alterered from elsewhere given its a highscore. you wouldnt want it being changed by anything other than the playing the game. but i also take your point. thank you!

3      

Right of the bat: You should add .ignoreSafeArea() to make the background cover the entire screen(also consider using a gradient, it makes the background look awesome if done correctly). Result:

        ZStack{
            Color.gray
                .ignoresSafeArea()
                //code
                }

Also if the game asks you to lose and if you chose the same thing as the computer chose you get a point. I presume this is unwanted because you didn't actually lose, but instead tied. I'm not going to provide an answer unless you ask me explicitly since you should try doing it yourself. Finally consider adding background color to buttons and images to make them easier to see.

2      

@tomn  

This is how I tackled Day 25 Challenge.

Any feedback for improvements welcome and much appreciated!

Rock paper & scissors

// displays the rock, paper or scissors move stack containing symbol and caption
struct MoveStack: View {
    var moveName: String
    var emojiName: String
    var isHighlighted: Bool

    var body: some View {
        VStack {
            Image(systemName: "\(emojiName)")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: isHighlighted ? 60: 40)
                .padding(.init(top: 0, leading: 10, bottom: 0, trailing: 10))
                .foregroundColor(isHighlighted ? .white : .accentColor)

            Text("\(moveName)")
                .foregroundColor(isHighlighted ? .white : .accentColor)
                .font(.subheadline)
        }
    }
}

// styles the player name "Computer" or "You"
struct PlayerName: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.title3)
            .foregroundColor(.white)
            .padding()
        //.background(.blue)
            .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}
extension View {
    func playerNameStyle() -> some View {
        modifier(PlayerName())
    }
}

struct ContentView: View {
    // store 3 possible moves for this game, along with winning moves
    let appMoves = ["Rock", "Paper", "Scissors"]
    let winningMoves = ["Paper", "Scissors", "Rock"]

    // 3 emojis that best represent rock, paper & scissors
    let emojis = ["bag", "note.text", "scissors"]

    // app shows its move, initialized to a random integer between 0 and 2
    @State private var computerMove : Int = Int.random(in: 0...2)

    // app asks player to win or lose
    @State private var shouldWin : Bool = Bool.random()

    // showingScore shows score alert. gameOver shows final score and game reset alert.
    @State private var showingScore = false
    @State private var gameOver = false

    // goes in the title of score alerts.
    @State private var scoreTitle = ""

    // keeps track of player score. Reset to 0 for new game.
    @State private var playerScore = 0

    // question counter, keep track of progress. Reset to 0 for new game.
    @State private var currentQuestion = 1

    // number of questions (or turns) to go
    let numberOfQuestions = 10

    // colorize the navigation title
    init() {
        UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.white]
        UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white]
    }

    var body: some View {
        NavigationView {
            ZStack {
                AngularGradient(gradient: Gradient(colors: [.black, .blue, .black, .black, .black]), center: .center)
                    .ignoresSafeArea()
                VStack {
                    HStack {
                        Spacer()
                        VStack {

                            Text("Computer")
                                .playerNameStyle()
                            ForEach(0...2, id: \.self) { i in
                                MoveStack(moveName: appMoves[i], emojiName: emojis[i], isHighlighted: i == computerMove ? true : false)
                            }

                        }
                        Spacer()
                        VStack {
                            Text(shouldWin ? "Win it" : "Lose it")
                                .font(.title.bold())
                            Text("Choose your move accordingly")
                        }
                        Spacer()
                        VStack {
                            Text("You")
                                .playerNameStyle()
                            ForEach(0...2, id: \.self) {i in
                                Button {
                                    // move was tapped
                                    moveTapped(i)
                                } label: {
                                    MoveStack(moveName: appMoves[i], emojiName: emojis[i], isHighlighted: false)
                                }

                            }

                        }
                        Spacer()

                        // score alert box
                            .alert(scoreTitle, isPresented: $showingScore) {
                                Button("Continue", action: askQuestion)
                            } message: {
                                Text("Your score is \(playerScore)")
                            }

                        // game over alert box
                            .alert("Game over", isPresented: $gameOver) {
                                Button("Reset game", action: resetGame)
                            } message: {
                                Text("Your score is \(playerScore)")
                            }
                    }
                    Spacer()
                    // score & turns
                    Text("Score: \(playerScore)").font(.title.bold())
                    Text("Turns: \(currentQuestion)/\(numberOfQuestions)")
                    // footer buttons
                    Spacer()
                }
            }

            .navigationTitle("Rock, Paper & Scissors")
            .foregroundColor(.white)

        }

    }

    // run this when player taps a move
    func moveTapped(_ number: Int) {
        var isCorrect : Bool
        // if computer is Rock, and player chose Paper, and player should win, then set isCorrect to TRUE
        switch (computerMove, number, shouldWin) {
        case (0,1, true) : isCorrect = true
        case (1,2, true) : isCorrect = true
        case (2,0, true) : isCorrect = true
        case (0,2, false) : isCorrect = true
        case (1,0, false) : isCorrect = true
        case (2,1, false) : isCorrect = true
        default : isCorrect = false
        }
        if isCorrect {
            playerScore += 1
            scoreTitle = "Correct"
        } else {
            scoreTitle = "Wrong!"
        }

        showingScore = true

        // increment the current question counter till it reaches the max number of turns / questions
        if currentQuestion < numberOfQuestions {
            currentQuestion += 1
        } else {
            gameOver = true
        }

    }

    // call the next turn
    func askQuestion() {
        shouldWin.toggle()
        computerMove = Int.random(in: 0...2)
    }

    // Game is over. Reset player's score and current question counter.
    func resetGame() {
        playerScore = 0
        currentQuestion = 1
    }
}

2      

@tomn

Well done again! Congrats with a new milestone !

Even being a new guy to coding I'd like to put my few words.

First of all:

  1. Great job with using Swich with 3 variables. That's cool !
  2. You've used custom modifier for text look which wasn't necessary. But you're implementing this option and it's great for cleaner code.
  3. Same goes for "MovesStack" struct. Great !

Now code:

    let winningMoves = ["Paper", "Scissors", "Rock"]

Looks like It has never been used in your code eventually. Or not ?

init() {
        UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.white]
        UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white]
    }

do you have any previous experience with Swift/UIKit ? I don't remember Paul has been showing us such a thing to use for modifying NavigationBarTitle's color before day 25. hmm ? :)

Maybe I've also seen a small bug after ending the round. I somehow got a poping alert with score 0, even though it should be the restart and new selection should begin so it shouldn't be possible to get the alert before making any choice. Hmmm.

Great job anyway !

3      

Tom asks for a code review:

This code is correct. But start looking at this with a critical eye. You have three variables and must keep the gameOver variable in sync with currentQuestion and numberOfQuestions.

Think to yourself, "If I'm changing the gameOver statement due to some condition, can I do this with a computed variable?" It could be dangerous to rely on IF statements to update gameOver state. What if you have another routine that updates currentQuestion via a Bonus mechanism, or a WildCard rule? It would be easy for your gameOver state to get out of sync with your other variables.

// Standard Approch
if currentQuestion < numberOfQuestions {
            currentQuestion += 1
        } else {
            gameOver = true
        }

Using a computed variable ensures that whenever currentQuestion is updated, the gameOver variable is automatically recalculated.

// More Swifty Approach
// When one variable changes, the other is automatically recalculated. 
var gameOver: Bool {
    currentQuestion >= numberOfQuestions
}

Keep coding!

3      

@tomn  

@ngkmn14 Thanks for the feedback and the advice.

  1. Winning Moves array: you are absolutely right. I was thinking of comparing 2 arrays... but then realized that it could be written in a simple formular such as 0 (rock) loses to 1 (paper). So I didn't actually need the 2nd array.
  2. UINavigationBar color: yes, up to day 31, Paul hasn't taught how to change a nav title text color. I googled it. :)
  3. 0 score alert at end of round: fix the bug!

@Obelix Definitely here to learn Swiftly ways of doing things. Much appreciated. gameOver was a @State private var, and was used in the following way.

@State private var gameOver = false
...
.alert("Game over", isPresented: $gameOver)

When I change gameOver to a computed property, .alert can't find '$gameOver' in scope anymore. Do I have to dynamically bind it somehow?

2      

@tomn  

@Obelix

I made the following changes. How does it look?

I couldn't figure out how to bind a computed property to .alert(), so I still had to do an if else statement in one of the methods to toggle showingGameover to trigger the game over alert.

But gameOver is now a computed property depending on the current question counter.

Also improved the UX a bit. Now the number of turns which I'm showing on the screen, only update after clicking away the score alert.

// showingScore shows score alert.
@State private var showingScore = false
@State private var showingGameover = false

// when gameOver is true, shows final score and game reset alert.
private var gameOver : Bool {
   currentQuestion > numberOfQuestions
}

...
.alert("Game over", isPresented: $showingGameover)
...
// call the next turn
func askQuestion() {
    currentQuestion = currentQuestion+1
    if gameOver {
        showingGameover = true
    } else {
        shouldWin.toggle()
        computerMove = Int.random(in: 0...2)
    }
}

2      

Tom upgrades his code!

I made the following changes. How does it look?

I think the code if gameOver is just what you should strive for! It's concise and to the point. Anyone can read this and understand your goals. Furthermore, because gameOver is a computed property, you do not have to add busy code ensuring this boolean stays in sync with the user's current question, and the number of questions in your game. Nice!

func askQuestion() {
    currentQuestion = currentQuestion+1  // <- try this...   currentQuestion += 1
    if gameOver {  // <- Very easy to read and keep in sync with your game state. Nice!
        showingGameover = true
    } else {
        shouldWin.toggle() // <- Maybe randomize this?
        computerMove = Int.random(in: 0...2)  // <- Now, take a close look here.
    }
}

But since you asked... now look at the computerMove line. Be critical of your design.

If you're playing Rock, Paper, or Scissors with someone and you ask them "what is your favorite move?"
What would you think if they replied two ??

Is two a valid option in Rock, Paper, or Scissors?

If my team were reviewing your code, they might ask you to reconsider this design. (Actually, they would insist you redesign this code!)

So what are the only valid options? And what have you learned in Swift that allows you to define a strict set of possible values?

See -> Day 3 Concepts

3      

@tomn  

@Obelix asks:

... what have you learned in Swift that allows you to define a strict set of possible values?

I'd then use an enum, make it CaseIterable, and call a random element on all cases of the enum.

Many thanks again.

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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.