4
\$\begingroup\$

I'm going through the eBook Intro to App Development with Swift by Apple and I am making the Rock, Paper, Scissors app in lesson 20.

This is the assignment that I got:

[...] Name the enum Sign. Add a calculated property to give the emoji that represents the Sign. [...] You need to be able to compare two Sign instances to give a GameState. ​ For example, a player’s .rock and the app’s .paper would give you .lose. Add an instance method to Sign that takes another Sign, representing the opponent’s turn, as a parameter. The method should return a GameState based on a comparison between self and the opponent’s turn. [...]

The code that I wrote works but I feel like this can be done a lot cleaner. Maybe it's Xcode's indentation that looks a bit weird to me, maybe there's a better solution?

I'll start with GameState.swift (these 4 gamestates were given in the instructions):

import Foundation

enum GameState {
    case start, win, lose, draw
}

And here's Sign.swift:

import Foundation

enum Sign {
    case rock, paper, scissors
    
    var emoji: String {
        switch self {
        case .rock: return "👊"
        case .paper: return "✋"
        case .scissors: return "✌️"
        }
    }
    
    func beats(otherSign: Sign) -> GameState {
        switch self {
        case .rock: do {
            if otherSign == .paper {
                return .lose
            } else if otherSign == .scissors {
                return .win
            }
            return .draw
            }
        case .scissors: do {
            if otherSign == .rock {
                return .lose
            } else if otherSign == .paper {
                return .win
            }
            return .draw
            }
        case .paper: do {
            if otherSign == .rock {
                return .win
            } else if otherSign == .scissors {
                return .lose
            }
            return .draw
            }
        }
    }
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Just wanted to add that you might want to name beats to playAgainst or something else, since it implies a boolean return type \$\endgroup\$ – user Jun 16 '20 at 19:44
  • \$\begingroup\$ @user good catch, I still need to master good naming principles :) \$\endgroup\$ – Max Jun 16 '20 at 20:21
  • 1
    \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. Consider posting a new question instead. \$\endgroup\$ – Mast Jun 20 '20 at 13:29
4
\$\begingroup\$

First, the do { ... } blocks inside the cases are not needed:

func beats(otherSign: Sign) -> GameState {
    switch self {
    case .rock:
        if otherSign == .paper {
            return .lose
        } else if otherSign == .scissors {
            return .win
        }
        return .draw
    case .scissors:

    // ...
}

But why use a switch-statement for self and an if-statement for otherSign? The if-statement is error-prone (we might e.g. forget a case), whereas the switch-statement perfectly matches the enum declaration and the compiler checks the exhaustiveness, i.e. that all cases are treated exactly once:

func beats(otherSign: Sign) -> GameState {
    switch self {
    case .rock:
        switch otherSign {
        case .paper:    return .lose
        case .scissors: return .win
        case .rock:     return .draw
        }
    case .scissors:

    // ...
}

This is already more compact than the original code, but still clear and easy to read.

Now we go one step further: A single switch-statement is sufficient because you can switch on a tuple in Swift:

func beats(otherSign: Sign) -> GameState {
    switch (self, otherSign) {
    case (.rock, .rock):     return .draw
    case (.rock, .paper):    return .lose
    case (.rock, .scissors): return .win
        
    case (.paper, .rock):     return .win
    case (.paper, .paper):    return .draw
    case (.paper, .scissors): return .lose
        
    case (.scissors, .rock):     return .lose
    case (.scissors, .paper):    return .win
    case (.scissors, .scissors): return .draw
    }
}

It is clear to the reader that (and how) all possible combinations are handled. Repeated or missing cases are detected by the compiler.

There is also another, completely different option. We can assign the integer values 0, 1, 2 to rock, paper, scissors, respectively, e.g. by making it an integer based enumeration:

enum Sign: Int {
    case rock, paper, scissors

    // ...
}

then the outcome of the comparison can efficiently be computed from the difference of the integer values:

func beats(otherSign: Sign) -> GameState {
    let diff = self.rawValue - otherSign.rawValue

    // ...
}

I'll leave it to you to work out the details (hint: modulo arithmetic), and to decide which variant your prefer: A (verbose) switch-statement with 9 cases, or a compact mathematical calculation.

\$\endgroup\$
3
  • \$\begingroup\$ Wow, very valuable answer! I never heard of a tuple before, for now I'll just assume that I can switch 2 identical datatypes and hope I encounter a better explanation in one of my next eBooks or courses. Modular arithmetic was giving me a bit of a headache, but I think I got a workable solution: 2 beats 1 beats 0 beats 2. So ... If the first Int is 1 bigger than the second Int, or the first Int is 2 lower than the second Int, it's a win. Else if the two Ints are the same it's a draw. Else it's a loss. \$\endgroup\$ – Max Jun 16 '20 at 20:16
  • 1
    \$\begingroup\$ @Max: You can switch on arbitrary tuples, the elements need not have the same type: switch (someInt, someString) { case (123, "abc"): ... \$\endgroup\$ – Martin R Jun 16 '20 at 20:25
  • 1
    \$\begingroup\$ See for example medium.com/the-traveled-ios-developers-guide/…. – Another example (sorry for the self-promotion): stackoverflow.com/a/25280179/1187415 \$\endgroup\$ – Martin R Jun 16 '20 at 20:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.