How to Make a Game Like Wordle in SwiftUI: Part One

Learn how to create your own Wordle word-game clone in SwiftUI. Understand game logic as you build an onscreen keyboard and letter tile game board. By Bill Morefield.

5 (5) · 4 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Checking a Guess

Checking a guess will be more complicated, so you'll create the method in several steps. This method will:

  1. Verify the guess is complete and valid.
  2. Check the guess for letters that are in the correct position.
  3. Check if the remaining letters are not in the word or in the wrong position.
  4. Update the game status based on the results.

Add the following new method after deleteLetter():

func checkGuess() {
  // 1
  guard guesses[currentGuess].word.count == wordLength  else { return }

  // 2
  if !dictionary.isValidWord(guesses[currentGuess].letters) {
    guesses[currentGuess].status = .invalidWord
    return
  }
}

In this initial code:

  1. You ensure the guess has exactly five characters. If not, you'll return immediately, which ignores the player's action.
  2. You then use the Dictionary object to check the word against a longer list of words. If it's not present, you set the status of the guess to invalidWord and return.

Checking For In-Position Letters

At this point, you know you have a legitimate guess to validate.

To process the result of each letter of the guess, add the following code to the end of the checkGuess() method:

// 1
guesses[currentGuess].status = .complete
// 2
var targetLettersRemaining = Array(targetWord)
// 3
for index in guesses[currentGuess].word.indices {
  // 4
  let stringIndex = targetWord.index(targetWord.startIndex, offsetBy: index)
  let letterAtIndex = String(targetWord[stringIndex])
  // 5
  if letterAtIndex == guesses[currentGuess].word[index].letter {
    // 6
    guesses[currentGuess].word[index].status = .inPosition
    // 7
    if let letterIndex = 
      targetLettersRemaining.firstIndex(of: Character(letterAtIndex)) {
      targetLettersRemaining.remove(at: letterIndex)
    }
  }
}

There's a lot here, but each step isn't complicated:

  1. First, you mark the guess as complete.
  2. Next, you create an array from the characters that make up the target word. You use this array to better handle situations where a target word contains the same letter multiple times. Take the target word THEME, for example. The E appears twice. How then should you evaluate a word with three Es like EERIE? The convention you'll use is to show the final E as green because it is in the correct position, the first E as notInPosition and the second E as notInWord because there are only two Es and you've accounted for both when you reach that position. This shows the player had one E in the correct position and the word contains only one more E.
  3. You loop through all indexes in the word property of the current guess using the indices property on that object.
  4. For each letter, you get the letter in the target word at the same index position. You might think to pass the Integer index as a subscript to the string, but that won't work. Instead, you need a String.Index value as the subscript. The first line gets the String.Index that corresponds to the index integer offset in the string. You can then use this as a subscript to the string to get the letter you desire, casting it to a String in the process. If you think this seems more complicated than it should be, you're right.
  5. You compare the letter you carefully extracted in the previous step to the guessed letter at the current index.
  6. If they match, you set the status of the guessed letter of the current guess to inPosition.
  7. When the letters match, you also get the first index of letterAtIndex, after casting it to a Character, in the array of characters and unwrap it as letterIndex. If the value exists, which it always should, you then remove the letter from the targetLettersRemaining array.

Example showing the words THEME and EERIE

Checking for in-position letters first ensures they have priority over not-in-position letters in the guess.

Checking Remaining Letters

Now, you can check for letters that are in the word but not in the correct position. Add the following code to the end of the checkGuess() method:

// 1
for index in guesses[currentGuess].word.indices
  .filter({ guesses[currentGuess].word[$0].status == .unknown }) {
  // 2
  let letterAtIndex = guesses[currentGuess].word[index].letter
  // 3
  var letterStatus = LetterStatus.notInWord
  // 4
  if targetWord.contains(letterAtIndex) {
    // 5
    if let guessedLetterIndex =
      targetLettersRemaining.firstIndex(of: Character(letterAtIndex)) {
      letterStatus = .notInPosition
      targetLettersRemaining.remove(at: guessedLetterIndex)
    }
  }
  // 6
  guesses[currentGuess].word[index].status = letterStatus
}

There's a lot here:

  1. Again, you loop through the indices for the word, but you use the filter method on the array to get only the ones still in an unknown status. You don't want to check the ones you found to be in the correct position in the previous section of code again.
  2. For each index position, you get the letter for that position of the guess.
  3. You set a variable of LetterStatus to notInWord.
  4. Because you only care if the guessed letter appears in the target word, you can use the contains() method of the string to see if the letter appears anywhere in the word.
  5. As in the previous code block, you get the index of letterAtIndex in the targetLettersRemaining array. This time, there's no guarantee the letter will be there because you've removed some letters from the target word. If a value is found, you change the letterStatus variable to notInPosition and remove the element from the targetLettersRemaining array. Removing the letter from targetLettersRemaining means you will only mark the same numbers of letters as either inPosition or notInPosition as in the target word if more are guessed.
  6. You set the status of this GuessedLetter to the value of the letterStatus variable, which will still be notInWord unless changed in step five.

Updating the Game Status

After evaluating each letter in the guess, you can now check if the user guessed the word. Add the following code to the end of the method:

if targetWord == guesses[currentGuess].letters {
  status = .won
  return
}

If the guess is the same as the target word, you set the game status to won and return. If not, you now handle the cases where the guess was wrong. Add the following code to the method:

if currentGuess < maxGuesses - 1 {
  guesses.append(Guess())
  currentGuess += 1
} else {
  status = .lost
}

If the current guess is less than the number of allowed guesses, you append a new blank guess to the guesses array and add one to the current guess. If not, the player did not guess the word in time, so you set the game status to lost.

To use this new logic, add the following code above the default case in addKey(letter:):

case ">":
  checkGuess()

This calls the checkGuess() method when the player taps the Enter button. To connect the keyboard view and model, open KeyButtonView.swift in the KeyboardViews group. Look for the line // Button action and replace it with:

game.addKey(letter: key)

A separate KeyButtonView is created for each key on the virtual keyboard. When the player taps the button, the view calls addKey(letter:) in the game object, passing in the letter for that key.

Now, you can start integrating this game logic with your UI. First, though, select the entire extension method at the bottom of GuessingGame.swift. Now, use the Editor ▸ Structure ▸ Comment Selection menu command to uncomment it in a single step.

The extension contains a convenience initializer that allows you to provide the target word and several static methods that will produce a game in a partial, won and lost state. Looking at these, you see they set a known target word using the convenience initializer and then use the addKey(letter:) method to simulate a player. You'll use this for the SwiftUI previews. Now, get ready to play in the next section!