On the last two blog post, Love for enum & continue…, we build our base for a more advance talk. Which leads us to this blog post were we will implement our already learned knowledge from those two blog post to build a case study here. No doubt enum plays an important role on Swift learning. A case study buildup through step by step cloud guide us to the why and how of enum on Swift, study through example and real time implementation will solidify our enum learning. So let us not delay any further. Let’s build the case study, A simple Tic tac toe game.
For a more in-depth knowledge on enum we can visit the enum hub page, where we listed all the blog post related with Swift enum.
Special note: To build up this case study our first choice was always enum
among other options, struct
or class
. We can always go through the apple blog on which the choosing among Value Type and Reference Type are beautifully describe. If you haven’t read that one, I will request you do.
So we gonna build a very basic game Tic tac toe. Ok no UI, sorry! Logic part only. Come on we are building a Swift enum based case study not a full phase Tic tac toe with example UI. And if you have the brain part then how long to build the UI. Ready?
Background
- Player
- Why Player not a
class
- Player not a
struct
, why - Why Player not a
Bool
- Square
- Square to be a
RawRepresentable
- Winning streak
- Game states
- Why to use associated Value
- Game state as
CustomStringConvertible
- Logical part of game
- Game play
- TDD teaser 🙈 🙉 🙊
Player
Tic tac toe is a two player game. So two cases, Perfect for enum
:
enum Player {
case First
case Second
}
Q/A:
- Why do we choose
enum
overstruct
andclass
? - How about
Bool
as it has only two possibilities?
class
, Why not
We do not need any instance of NSObject
and also we are not to change the status of a player, first player will not be second and vice versa. So class
is not an option.
struct
, why not
Ok from the above assertion it should be a value type object. Now we can clearly see it has two state, First and Second, and at a time only one will be active, so this makes it very simple to choose. struct
can be used if there are some vars or state which will live together. So struct
not a good option.
Bool
, Why not
Player
has two state, First and Second. So isFirstPlayer: Bool
instead of Player
is a good option, ahh cloud be! But wait, will we always use something like isFirstPlayer? doSomething : doAnotherThing
whenever we need to make a decision? even to assign a var? Bool
type checking is not pretty 😉.
Square
On 3X3 Tic tac toe, there will be total of 9 square. We can define them as follows:
enum Square {
case One_One, One_Two, One_Three
case Two_One, Two_Two, Two_Three
case Three_One, Three_Two, Three_Three
}
Again we do not want value mutability, class
. And Square
will have only one instance and that instance do not have any other var/instance to live together, struct
.
Additional Q/A
Why Array
or any other collection type was not chosen instead of the enum
, Square
?
We already answered that on Love for Enum blog post.
Square to be a RawRepresentable
So far we have only declared the nine squares, but have not assign any value to those squares. We need to have some kind of identifier to uniquely identify each square.
So can we use Int
as the RawRepresentable
of Square
?
Hmm it solves the unique identifier issue. But there is more. We also need to compare the values among square to find the winning streak. And one of them is the diagonal winning streak. Single value Int
will not be enough. The winning streak is always a two dimensional value to be compared, unless someone wants to write down all the winning combination/streak one after another on a static way.
So now we know we need to have a two dimensional value, that mean tuple
. We can call this tuple
as Position
. So here we go:
extension Square: RawRepresentable{
typealias Position = (row: Int, column: Int)
var rawValue: Position{
switch self {
case .One_One: return (row: 1, column: 1)
case .One_Two: return (row: 1, column: 2)
case .One_Three: return (row: 1, column: 3)
case .Two_One: return (row: 2, column: 1)
case .Two_Two: return (row: 2, column: 2)
case .Two_Three: return (row: 2, column: 3)
case .Three_One: return (row: 3, column: 1)
case .Three_Two: return (row: 3, column: 2)
case .Three_Three: return (row: 3, column: 3)
}
}
init?(rawValue: Position) {
switch rawValue {
case (row: 1, column: 1): self = .One_One
case (row: 1, column: 2): self = .One_Two
case (row: 1, column: 3): self = .One_Three
case (row: 2, column: 1): self = .Two_One
case (row: 2, column: 2): self = .Two_Two
case (row: 2, column: 3): self = .Two_Three
case (row: 3, column: 1): self = .Three_One
case (row: 3, column: 2): self = .Three_Two
case (row: 3, column: 3): self = .Three_Three
default: return nil
}
}
On the above Square
is conforming the RawRepresentable
protocol. As a result Square
need to provide the conversion from and into the rawValue
, Position
. We had a detail talk on previous enum blog post.
Winning streak
We can have one of the three winning streak, horizontal or vertical or diagonal. So by considering two thing, immutable values(discard class
) and only one among three case will be active(discard struct
), we can use enum
.
enum WinningStreak: String{
case horizontal
case vertical
case diagonal
}
We are setting String
as the raw type of WinningStreak
, because later down the road we will print the winning combination. BTB we already published the raw value blog talk.
horizontal
WinningStreak
can happen either on the first, second or third row. Similarly vertical
and diagonal
has multiple possibilities. We are discarding those possibilities for making this blog post concise.
Game states
We can have either inProgress
, draw
or a win
state for a game. Again one of three options/case. And immutable value is not needed. So what do you think? It should be an enum
:
enum GameState {
case inProgress
case draw
case win(Player, WinningStreak)
}
The win
case have an associated value and it is a tuple
of Player
and WinningStreak
.
Why associated Value
Q/A: Why the associated value on win
case and why others don’t have the associated value?
Hmm seems like a silly question, but not so. Answer to this question will help us to add associated value to our case when needed.
The functionality of associatedValue is to provide a way to that corresponding case to have an extra storage for additional info. You can read more on our previous blog post.
Now the inProgress
and draw
do not need any more info. Because both of these two cases are stating the game state where both the Payer
s are involved. Also the game state is self explanatory for those two cases.
But on the case of win
two obvious question comes up. Who win? and which combination was used? So these two questions are pointing to Player
and WinningStreak.
Game state as CustomStringConvertible
GameState
is the end of game, once we got either draw
or win
, we can publish the game result on a String
format. For inProgress
we can just say the game is still going. The CustomStringConvertible
protocol will serve us the best on this regard. It has a description
var, which we need to implement for all the cases of GameState
.
extension GameState: CustomStringConvertible{
var description: String{
switch self {
case .inProgress:
return "Game is running"
case .draw:
return "Game ended up in a draw"
case .win(let player):
return "\(player) won on \(streak) combination"
}
}
}
Game logic
Now we need to calculate the WinningStreak
. First We will write down a function to calculate the result. We will feed this function with the sequence of a single player.
func result(for sequence: [Square]) -> WinningStreak?{
guard sequence.count == 3, sequence[0] != sequence [1], sequence[1] != sequence[2], sequence[2] != sequence[0] else { return nil }
if sequence[0].rawValue.row == sequence[1].rawValue.row && sequence[1].rawValue.row == sequence[2].rawValue.row {
return .horizontal
}
if sequence[0].rawValue.column == sequence[1].rawValue.column && sequence[1].rawValue.column == sequence[2].rawValue.column {
return .vertical
}
if (sequence[0].rawValue.row == sequence[0].rawValue.column && sequence[1].rawValue.row == sequence[1].rawValue.column && sequence[2].rawValue.row == sequence[2].rawValue.column) ||
Set(arrayLiteral: sequence) == Set(arrayLiteral: [Square.One_Three, .Two_Two, .Three_One]){
return .diagonal
}
return nil
}
We will return nil
on three situation.
- If the sequence is lower or higher than 3.
- If any two of the sequence elements are same.
- And if no
WinningStreak
can be formed from the sequence.
The horizontal
and vertical
checking is pretty simple. Same row for all three combination will result on horizontal
win, on the other hand three same column will result on vertical
win.
For diagonal
win, there are two possibilities. On one possibility all the elements of sequence will have the same row and column individually. And another possibility is kind of a static check of the square.
For the static check we are using Set
because we do not want any fixed chronology of elements, they can appear on any combination.
Game play
This is the last part of our Tic tac toe. We already laid down the necessary parts of the game. Now Game On 👊.
We will call our game play as TicTacToe
. We will put one Square
at a time. And will get the GameState
immediately. Once we have a win we will also get that.
class TicTacToe{
private var sequence = [Square]()
func progress(_ square: Square) -> GameState? {
guard sequence.count < 9 else { return nil }
sequence.append(square)
guard sequence.count > 4 else { return .inProgress }
var firstPlayerSequence = [Square]()
var secondPlayerSequence = [Square]()
for (index, square) in sequence.enumerated(){
index % 2 == 0 ? firstPlayerSequence.append(square) : secondPlayerSequence.append(square)
}
if let streak = result(for: firstPlayerSequence) {
return .win(Player.First, streak)
}else if let streak = result(for: secondPlayerSequence){
return .win(Player.Second, streak)
}else{
return .draw
}
}
}
We used class
here rather than enum
or struct
. Because we need to mutate the sequence
var. So as it is mutating it’s own state we need to use class
.
Now let us move our focus to progress
function. As sequence
will have max 9 elements so any more than that will result in nil
. Once sequence
got the 4 elements we will start calculating the WinningStreak
. On the process of calculating the WinningStreak
we are separating the elements into two array. Even index element goes to firstPlayerSequence
and odd populates the secondPlayerSequence
.
End talk
So that is it. Tic tac toe is ready. Again our main concern here was to watch the use of enum
, not the Tic tac toe game. So there are still some works can be done to improve this game. You can find the source code on GitHub. This Swift enum case study based talk will not be successful unless the example code been studied. So please go through the example code of this case study to have a solid idea on Swift enum implementation.
TDD teaser
If you already saw the code on GitHub, you will see a lot of test codes, Actually 1.4 times than the source code. What happens there !!!
Well it was TDD. This project was started using TDD and continue till the end. To be honest it is always thrilling to write test code first then the production code. If you are interested then you can have a look at the Cycles of TDD on Robert C. Martin (Uncle Bob)’s blog.
On some future we will have a TDD series on iOS using Swift. And I can guarantee you one thing, it will not be a blog series it will be a video series. So much talk is not possible to be written down on just one or two blog post. Best possible way is to describe through video series. Also the short code to run the test on a playground file directly taken from Writing unit tests on playground blog post written by Sundell.
So thats actually ends our talk with enum
. See you around. Till then take care. 🖖
2 thoughts on “Tic tac toe, through enum”