Day 37 - 100 Days of Swift

5 minute read

Project 8 (part 2)

Day 37 is the second part of the eighth project. Today you add all the logic to make the game playable. You add the targets to all the buttons you made yesterday, you add a function to “load a level” into memory. You implement the functions that all the buttons target. And you add a property observer to score to update scoreLabel whenever it changes.

The first thing you do is add a couple new variables and three empty functions that the buttons will target:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var activatedButtons: [UIButton] = []
var solutions: [String] = []
var score = 0
var level = 1

@objc func letterTapped(_ sender: UIButton) {

}

@objc func submitAnswer(_ sender: UIButton) {

}

@objc func clearAnswer(_ sender: UIButton) {

}

Then you add targets to the the buttons:

1
2
3
4
5
6
7
8
9
10
11
12
// In loadView, where each button is set up
submit.addTarget(self,
                 action: #selector(submitAnswer),
                 for: .touchUpInside)

clear.addTarget(self,
                action: #selector(clearAnswer),
                for: .touchUpInside)

letterButton.addTarget(self,
                       action: #selector(letterTapped),
                       for: .touchUpInside)

Then you add a function to load the level from a text file. The text file looks like this:

1
2
3
4
5
6
7
HA|UNT|ED: Ghosts in residence
LE|PRO|SY: A Biblical skin disease
TW|ITT|ER: Short but sweet online chirping
OLI|VER: Has a Dickensian twist
ELI|ZAB|ETH: Head of state, British style
SA|FA|RI: The zoological web
POR|TL|AND: Hipster heartland

Because of the very specific format of that text file, we can split on the string with .components(seperatedBy:) on \n, or : or |" to get to the individual elements that we care about. Here is what that looks like in the loadView method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var clueString = ""
var solutionString = ""
var letterBits: [String] = []

guard let levelFileURL = Bundle.main.url(forResource: "level\(level)",
                                         withExtension: "txt") else { return }
guard let levelContents = try? String(contentsOf: levelFileURL) else { return }

// Split things up by line and shuffle their order
var lines = levelContents.components(separatedBy: "\n")
lines.shuffle()

for (index, line) in lines.enumerated() {
    // Split the line into the answer and the clue
    let parts = line.components(separatedBy: ": ")
    let answer = parts[0]
    let clue = parts[1]

    // Add the clue to the clue string
    clueString += "\(index + 1). \(clue)\n"

    // Get the answer itself, without the pipes
    let solutionWord = answer.replacingOccurrences(of: "|",
                                                   with: "")
    // Add that to the solution string, and array
    solutionString += "\(solutionWord.count) letters\n"
    solutions.append(solutionWord)

    // Get the word parts to display in the buttons
    let bits = answer.components(separatedBy: "|")
    letterBits += bits
}

// Trim off the extra newlines at the end
cluesLabel.text = clueString.trimmingCharacters(in: .whitespacesAndNewlines)
answersLabel.text = solutionString.trimmingCharacters(in: .whitespacesAndNewlines)

letterBits.shuffle()

// Put the word parts into the buttons
if letterBits.count == letterButtons.count {
    for i in 0..<letterButtons.count {
        letterButtons[i].setTitle(letterBits[i],
                                  for: .normal)
    }
}

Next, you implement the letterTapped method:

1
2
3
4
guard let buttonTitle = sender.titleLabel?.text else { return }
currentAnswer.text = currentAnswer.text?.appending(buttonTitle)
activatedButtons.append(sender)
sender.isHidden = true

And the clearTapped method:

1
2
3
4
5
6
7
currentAnswer.text = ""

for button in activatedButtons {
    button.isHidden = false
}

activatedButtons.removeAll()

And finally, the submitAnswer method. In this one, you figure out which position the solution is in (if it is a good solution) and replace the line at that index in the solution string. If the user has found all the solutions, you present an alert to go to the next level:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
guard let answerText = currentAnswer.text else { return }

guard let solutionPosition = solutions.firstIndex(of: answerText) else { return }

activatedButtons.removeAll()
var splitAnswers = answersLabel.text?.components(separatedBy: "\n")
splitAnswers?[solutionPosition] = answerText
answersLabel.text = splitAnswers?.joined(separator: "\n")

currentAnswer.text = ""
score += 1

if score % 7 == 0 {
    let alertController = UIAlertController(title: "Well done!",
                                            message: "Are you ready for the next level?",
                                            preferredStyle: .alert)
    let action = UIAlertAction(title: "Let's go!",
                               style: .default,
                               handler: levelUp)
    alertController.addAction(action)
    present(alertController, animated: true)
}

Then you just need to add levelUp(action:):

1
2
3
4
5
6
7
8
9
10
func levelUp(action: UIAlertAction) {
    level += 1
    solutions.removeAll(keepingCapacity: true)

    loadLevel()

    for button in letterButtons {
        button.isHidden = false
    }
}

With that, the game works. The only problem is that it doesn’t show the user’s score, so you add a didSet observer to score that updates scoreLabel every time it changes:

1
2
3
4
5
var score = 0 {
    didSet {
        scoreLabel.text = "Score: \(score)"
    }
}

Then you have a finished, playable game that looks like this:

Screenshot of working app submitting a word.
Screenshot of working app on last word.
Screenshot of working app showing end of level alert.

You can find my version of this project at the end of day 37 on Github here.

Reflections

Today stood out to me because of the complexity of game logic that can be encapsulated in a relatively small amount of code. The core of this game is pretty much done and could be extended a lot by just writing new levels for it, without adding any code at all. That is pretty cool.