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
Dictionary
object to check the word against a longer list of words. If it's not present, you set the status of the guess toinvalidWord
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:
- 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
notInPosition
and the second E asnotInWord
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. - You loop through all indexes in the
word
property of the current guess using theindices
property 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
index
as a subscript to the string, but that won't work. Instead, you need aString.Index
value as the subscript. The first line gets theString.Index
that corresponds to theindex
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 aString
in 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 thetargetLettersRemaining
array.
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
filter
method on the array to get only the ones still in anunknown
status. 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
LetterStatus
tonotInWord
. - 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
letterAtIndex
in thetargetLettersRemaining
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 theletterStatus
variable tonotInPosition
and remove the element from thetargetLettersRemaining
array. Removing the letter fromtargetLettersRemaining
means you will only mark the same numbers of letters as eitherinPosition
ornotInPosition
as in the target word if more are guessed. - You set the status of this
GuessedLetter
to the value of theletterStatus
variable, which will still benotInWord
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!