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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Game Like Wordle in SwiftUI: Part One
35 mins
- Getting Started
- Guessing a Letter
- Displaying a Guessed Letter
- Making a Guess
- Showing the Guess
- Building the Game Logic
- Connecting the Model and App
- Checking a Guess
- Checking For In-Position Letters
- Checking Remaining Letters
- Updating the Game Status
- Building the Gameboard View
- Starting a New Game
- Where to Go From Here
Checking a Guess
Checking a guess will be more complicated, so you'll create the method in several steps. This method will:
- Verify the guess is complete and valid.
- Check the guess for letters that are in the correct position.
- Check if the remaining letters are not in the word or in the wrong position.
- 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:
- You ensure the guess has exactly five characters. If not, you'll return immediately, which ignores the player's action.
- You then use the
Dictionaryobject to check the word against a longer list of words. If it's not present, you set the status of the guess toinvalidWordand 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:
- First, you mark the guess as complete.
- 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
notInPositionand the second E asnotInWordbecause 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. - You loop through all indexes in the
wordproperty of the current guess using theindicesproperty on that object. - For each letter, you get the letter in the target word at the same index position. You might think to pass the Integer
indexas a subscript to the string, but that won't work. Instead, you need aString.Indexvalue as the subscript. The first line gets theString.Indexthat corresponds to theindexinteger offset in the string. You can then use this as a subscript to the string to get the letter you desire, casting it to aStringin the process. If you think this seems more complicated than it should be, you're right. - You compare the letter you carefully extracted in the previous step to the guessed letter at the current index.
- If they match, you set the status of the guessed letter of the current guess to
inPosition. - 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 asletterIndex. If the value exists, which it always should, you then remove the letter from thetargetLettersRemainingarray.
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:
- Again, you loop through the indices for the word, but you use the
filtermethod on the array to get only the ones still in anunknownstatus. You don't want to check the ones you found to be in the correct position in the previous section of code again. - For each index position, you get the letter for that position of the guess.
- You set a variable of
LetterStatustonotInWord. - 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. - As in the previous code block, you get the index of
letterAtIndexin thetargetLettersRemainingarray. 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 theletterStatusvariable tonotInPositionand remove the element from thetargetLettersRemainingarray. Removing the letter fromtargetLettersRemainingmeans you will only mark the same numbers of letters as eitherinPositionornotInPositionas in the target word if more are guessed. - You set the status of this
GuessedLetterto the value of theletterStatusvariable, which will still benotInWordunless 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!
