|
For those who didn't read all Hints provided, here is a rather dorky solution that adopts a "if then" logic. Not elegant, but it works.
struct ContentView: View {
@State private var botMoves = ["circle.fill", "hand.raised", "scissors"] //rock, paper, scissors
@State private var appMove = Int.random(in: 0...2) //the app's current choice, used along with botMoves.
@State private var winOrLose = Bool.random() //decides if the player should pick win or lose. True is win false is lose.
@State private var totalPlayed = 0
@State private var showingScore = false
@State private var scoreTitle = ""
@State private var userScore = 0
@State private var gameFinished = false //whether the game is finished at 8 questions
var currentMove : String {
botMoves[appMove]
}
let botMoveStrings = ["rock", "paper", "scissors"]
var body: some View {
VStack(spacing: 15) {
Spacer()
Text("")
Text("Rock Paper Scissors").font(.largeTitle.weight(.bold)).foregroundColor(.blue)
Spacer()
Text("The system's current move is: \(currentMove == "circle.fill" ? "rock" : currentMove == "hand.raised" ? "paper" : "scissors")")
Spacer()
Text("You want to: \(winOrLose ? "Win" : "Lose")")
VStack {
Spacer()
ForEach(0..<3) {number in
Button {
tapped(number)
} label: {
NumberImage(move: botMoves[number]).padding(20)
}
}
.alert(scoreTitle, isPresented: $showingScore) {
Button("Continue", action: askQuestion)
} message: {
Text("Your socre is \(userScore)")
}.buttonStyle(.borderedProminent).tint(.mint)
Spacer()
Button("Restart") {
gameFinished = true
}
.alert("You have finished all \(totalPlayed) games", isPresented: $gameFinished) {
Button("Restart", action: reset)
//Button("Cancel", role: .cancel) {} //add a cancel option/button
} message: {
Text("Your final score is \(userScore)")
}
Spacer()
Text("Your score is: \(userScore)")
Spacer()
Text("Your have played \(totalPlayed) games")
Spacer()
}
}
.ignoresSafeArea()
}
//resets the game by shuffling up botMoves and picking a new correct answer
func askQuestion() {
botMoves.shuffle()
winOrLose = Bool.random()
appMove = Int.random(in: 0...2)
}
func reset() {
userScore = 0
totalPlayed = 0
botMoves.shuffle()
}
func tapped(_ number: Int) {
if totalPlayed == (10 - 1) {
gameFinished = true
}
else {
if winOrLose == true {
if currentMove == "circle.fill" && botMoves[number] == "hand.raised" {
userScore += 1
scoreTitle = "Correct! You got 1 point"
}
else if currentMove == "hand.raised" && botMoves[number] == "scissors" {
userScore += 1
scoreTitle = "Correct! You got 1 point"
}
else if currentMove == "scissors" && botMoves[number] == "circle.fill" {
userScore += 1
scoreTitle = "Correct! You got 1 point"
}
else {
userScore -= 1
scoreTitle = "Wrong! You lost 1 point"
}
}
else if winOrLose == false {
if currentMove == "circle.fill" && botMoves[number] == "scissors" {
userScore += 1
scoreTitle = "Correct! You got 1 point"
}
else if currentMove == "hand.raised" && botMoves[number] == "circle.fill" {
userScore += 1
scoreTitle = "Correct! You got 1 point"
}
else if currentMove == "scissors" && botMoves[number] == "hand.raised" {
userScore += 1
scoreTitle = "Correct! You got 1 point"
}
else {
userScore -= 1
scoreTitle = "Wrong! You lost 1 point"
}
}
}
totalPlayed += 1
showingScore = true
}
}
struct NumberImage: View {
var move: String
var body: some View {
Image(systemName: move).renderingMode(.original).clipShape(Capsule()).shadow(radius: 5)
}
}
|
|
Thanks for posting, it was helpful when I was struggling with the tapped function and the ternary operator in the text view. Based on Paul's hint about having an array for winning moves, I was able to simplify the logic somewhat, though it took me a bit longer than it should have π
My code is below, but first a question for anyone who knows: When using ForEach to create three buttons, I first tried iterating through the array with ForEach(choices, id: \.self) {Text($0)} but kept getting an error, so I ended up just using the range 0..<3. What was I doing wrong?
Lastly, my buttonTapped function isn't quite right: on the 10th/final guess, if I get the correct answer, it doesn't add 1 to my score. Anyone know what the solution is?
struct ContentView: View {
@State private var choices = ["πͺ¨", "π", "βοΈ"] // array of choices for the app to choose from
@State private var choicesWin = ["π", "βοΈ", "πͺ¨"] // array of choices to win against the app
@State private var choicesLose = ["βοΈ", "πͺ¨", "π"] // array of choices to lose against the app
@State private var appChoice = Int.random(in: 0...2) // randomly select from the choices array
@State private var shouldWin = Bool.random() // bool that will toggle between true (win) and false (lose) each turn
@State private var showingScore = false // for alert when to show score
@State private var userScore = 0 // track user's score and display in alert
@State private var questionNumber = 0 // track question count
@State private var gameFinished = false // for alert when game is finished after 10 questions
@State private var scoreTitle = "" // for alert title
var currentMove: String { // computed variable of the app's current move
choices[appChoice]
}
var winningMove: String { // computed variable of the winning move
choicesWin[appChoice]
}
var losingMove: String { // computed variable of the losing move
choicesLose[appChoice]
}
var body: some View {
VStack(spacing: 15) {
Spacer()
Text("Rock, paper, scissors!")
.font(.largeTitle.weight(.heavy))
.padding(.bottom, 30)
Text("Your score: \(userScore)")
Text("Total guesses: \(questionNumber)")
Text("The computer chose \(currentMove) and wants you to \(shouldWin ? "win" : "lose").")
.font(.title.weight(.regular))
.padding(30)
.padding(.bottom, 20)
ForEach(0..<3) { number in // three buttons based on the items in the choices array
Button {
buttonTapped(number)
} label: {
Text(choices[number])
}
.font(.system(size: 60))
}
Spacer()
}
.alert(scoreTitle, isPresented: $showingScore) { // alert to show the score after each question
Button("Continue", action: askQuestion)
}
.alert(scoreTitle, isPresented: $gameFinished) { // alert to show the final score when game is finished
Button("Restart", action: reset)
} message: {
Text("Final score: \(userScore)/\(questionNumber)")
}
}
func askQuestion() {
choices.shuffle()
shouldWin.toggle()
appChoice = Int.random(in: 0...2)
}
func reset() {
userScore = 0
questionNumber = 0
choices.shuffle()
showingScore = false
}
func buttonTapped(_ number: Int) { // code to run when a button is tapped
if questionNumber == 9 {
scoreTitle = "Finished!"
gameFinished = true
}
else {
if shouldWin == true {
if winningMove == choices[number] {
userScore += 1
scoreTitle = "Correct! You got 1 point"
showingScore = true
} else {
scoreTitle = "Wrong!"
showingScore = true
}
}
else if shouldWin == false {
if losingMove == choices[number] {
userScore += 1
scoreTitle = "Correct! You got 1 point"
showingScore = true
} else {
scoreTitle = "Wrong!"
showingScore = true
}
}
}
questionNumber += 1
}
}
|
|
When using ForEach to create three buttons, I first tried iterating through the array with ForEach(choices, id: \.self) {Text($0)} but kept getting an error, so I ended up just using the range 0..<3. What was I doing wrong?
Depends on what the error was.
|
|
@roosterboy, this chunk of code returned two errors. Let me know if you need more info.
ForEach(choices, id: \.self) { number in // three buttons based on the items in the choices array
Button {
buttonTapped(number) // error on this line: Cannot convert value of type 'String' to expected argument type 'Int'
} label: { // error on this line: Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body
Text($0)
}
|
|
My code above had a few bugs, so I'm reposting it. To resolve the problem of a point not being added if the 10th/final guess was correct, I split the buttonTapped function into two functions: the first to check for the correct answer and +1 to the score if correct and the second to check if questionNumber == 10 is true, which will tell us if the game is finished.
struct ContentView: View {
@State private var choices = ["πͺ¨", "π", "βοΈ"] // array of choices for the app to choose from
@State private var choicesWin = ["π", "βοΈ", "πͺ¨"] // array of choices to win against the app
@State private var choicesLose = ["βοΈ", "πͺ¨", "π"] // array of choices to lose against the app
@State private var appChoice = Int.random(in: 0...2) // randomly select from the choices array
@State private var shouldWin = Bool.random() // bool that will toggle between true (win) and false (lose) each turn
@State private var showingScore = false // for alert when to show score
@State private var userScore = 0 // track user's score and display in alert
@State private var questionNumber = 0 // track question count
@State private var gameFinished = false // for alert when game is finished after 10 questions
@State private var scoreTitle = "" // for alert title
var currentMove: String { // computed variable of the app's current move
choices[appChoice]
}
var winningMove: String { // computed variable of the winning move
choicesWin[appChoice]
}
var losingMove: String { // computed variable of the losing move
choicesLose[appChoice]
}
var body: some View {
VStack(spacing: 15) {
Spacer()
Text("Rock, paper, scissors!")
.font(.largeTitle.weight(.heavy))
.padding(.bottom, 30)
Text("Your score: \(userScore)")
Text("Total guesses: \(questionNumber)")
Text("The computer chose \(currentMove) and wants you to \(shouldWin ? "win" : "lose").")
.font(.title.weight(.regular))
.padding(30)
.padding(.bottom, 20)
ForEach(0..<3) { number in // three buttons based on the items in the choices array
Button {
buttonTapped(number)
isGameFinished()
} label: {
Text(choices[number])
}
.font(.system(size: 60))
}
Spacer()
}
.alert(scoreTitle, isPresented: $showingScore) { // alert to show the score after each question
Button("Continue", action: askQuestion)
}
.alert(scoreTitle, isPresented: $gameFinished) { // alert to show the final score when game is finished
Button("Restart", action: reset)
} message: {
Text("Final score: \(userScore)/\(questionNumber)")
}
}
func askQuestion() {
//choices.shuffle() removed this line because I think it was messing up my choicesWin and choicesLose arrays
shouldWin = Bool.random()
appChoice = Int.random(in: 0...2)
}
func reset() {
userScore = 0
questionNumber = 0
//choices.shuffle() removed this line; see above
showingScore = false
}
func buttonTapped(_ number: Int) { // first function to run when a button is tapped
if shouldWin == true {
if winningMove == choices[number] {
userScore += 1
scoreTitle = "Correct! You got 1 point"
showingScore = true
} else {
scoreTitle = "Wrong!"
showingScore = true
}
}
else if shouldWin == false {
if losingMove == choices[number] {
userScore += 1
scoreTitle = "Correct! You got 1 point"
showingScore = true
} else {
scoreTitle = "Wrong!"
showingScore = true
}
}
questionNumber += 1
}
func isGameFinished() { // second function to run when button is tapped
if questionNumber == 10 {
showingScore = false
scoreTitle = "Finished!"
gameFinished = true
} else {
// do nothing
}
}
}
|
|
Alex asks:
When using ForEach to create three buttons, I first tried iterating through the array with ForEach(choices, id: .self) {Text($0)} but kept getting an error, so I ended up just using the range 0..<3. What was I doing wrong?
Part of your SwiftUI journey is learning the code. Another KEY part is learning to read the error codes.
Think of ForEach as a factory. Its job is to create views from raw materials that you provide. You want your ForEach factory to create some buttons. So ask yourself, what raw materials do you need to provide to the Button ?
private var choices = ["πͺ¨", "π", "βοΈ"] // Always ask yourself, what TYPE is this variable??:
// Here you want the ForEach to build some views for you.
// ForEach is a factory. You give it something and it builds a view for each item you give it.\
// What are the RAW materials you are sending to this factory?
ForEach(choices, id: \.self) { number in // What TYPE is choices?
Button {
// The error message is VERY clear here.
// Your factory machine expects an Int, but you are providing a String
buttonTapped(number) // error on this line: Cannot convert value of type 'String' to expected argument type 'Int'
} label: {
Text($0)
}
I think you tripped yourself. In the ForEach declaration, you are sending in three strings. Yet, you call the internal variable number .
After that, in your brain, you think you're working with a number. This is NOT what you're working with. Then you further confuse yourself by asking "Why doesn't buttonTapped(number) work?" It doesn't work because you are NOT giving it a number.
Think about using specific variable names. Be creative! What would be a better name than number? Maybe gamePiece, or playersOption?
|
|
@Obelix, thanks for your advice. After some testing, I think I got it working. I changed number to option , which is a String, since I'm iterating through the options of the choices array, each of which is a string. I then adjusted the buttonTapped function, replacing _ number: Int with _ option: String and choices[number] with option . The ForEach block is below:
ForEach(choices, id: \.self) { option in // three buttons based on the options in the choices array
Button {
buttonTapped(option)
isGameFinished()
} label: {
Text("\(option)")
Using Text($0) returns an error ("Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body"), so I think I need to review what that syntax actually means. But Text("\(option)") seems to work.
|
|
Alex finds another error:
Using Text($0) returns an error ("Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body"), so I think I need to review what that syntax actually means. But Text("(option)") seems to work.
Another error, another opportunity to learn how to interpret error messages! I agree, though. This one is not so straight forward. Let's have a closer look.
The error messages says:
closure type '() -> Text' expects 0 arguments.
Swift thinks you defined a closure that takes NO arguments, and returns a Text. Did you?
Did you provide a closure that takes ZERO arguments? By using the $0 argument, you're telling Swift that you're not going to use named arguments and that Swift should take care of this for you.
However, you declared your closure requires a String (specifically one of the elements in the choices array) and that you have named this parameter option .
ForEach(choices, id: \.self) { option in // Tell Swift this closure requires a parameter you have named: option
Button {
buttonTapped(option)
isGameFinished()
} label: {
Text($0) // Here you're telling Swift this closure does not accept parameters
}
So look again at the error message? Does it make sense now? The compiler is telling you the exact reason for the error. You provide a parameter named option , yet in the body of the closure you used the $0 syntax indicating your closure shouldn't accept named parameters.
|