Creating Game of Life on iOS
There is no model-independent test of reality. It follows that a well-constructed model creates a reality of its own. An example that can help us think about issues of reality and creation is the Game of Life, invented in 1970 by a young mathematician at Cambridge named John Conway.
— The Grand Design, by Stephen Hawking
Game of Life is a popular implementation of the automaton theory. It is not really a game where one player can play against the computer or where two or more players can play against each other. Game of Life is automation that plays by itself and evolves itself generation after generations. Similar to our universe!
The playing field is divided into rows and columns, ideally infinitely large.
The board looks something similar to a chess board. Each square is a cell that can occupy one of two states either alive or dead. We need to place an initial generation of living cells in the field.
Each square can be in one of two states: alive (shown in black) or dead (shown in white). Each square (Cell) has eight neighbors: up, down, left, right and diagonal neighbors.
There are three simple rules for creating our game universe.
- A square that is already alive can continue living to the next generation if it has two or three neighbors
- A square that is currently not alive and has exactly three live neighbors becomes alive on the next generation
- A square dies if it has zero or only one neighbor (dies from loneliness) and also it will die if it has more than three neighbors (dies from overpopulation)
These simple rules create a variety of complex structures from certain initial patterns over the course of the game. Some remain unchanged, some move through the same patterns, some of them grow or completely disappear.
Creating our game architecture
We want to create game logic completely view independently. We will be creating an object GameState that is responsible for iterating to the next generations, deciding whether a cell is dead or alive and managing the states of the game.
Let's create our cell first. It will be a Cell Model that represents the state of a square in our game (not to confuse it with a UITableViewCell or UICollectionViewCell from iOS).
Our cell knows its state of life and has two factory methods for creating an alive or a dead cell.
We also need a model that holds the current state of the game:
The GameState has a subscript that allows us to access its cell directly gameState[i] is the same as gameState.cells[i] using subscripts in this type of cases helps us to shorten up our code.
next, we need an iterate function that changes the states of our game through generations:
as we see from above we need a state function that will determine the next state of our cell based on the three rules explained above. We will be writing it using TTD in the following section.
Using Test-driven development
In this part, we will be presenting the importance of TDD and how it can be used to make development easier.
I hear software developers a lot of times saying that writing test won’t solve us all the bugs. Since we will still forget some edge cases to test. And most of the times they avoid writing the tests completely as they don’t see much benefit in doing that.
Writing tests is not about solving or avoiding all the bugs it’s about writing cleaner code, refactoring confidently, avoiding regression and make the code more understandable by looking at the test cases.
How many times you see yourself in the following scenario:
You see a big pile of bad code and you decide to refactor it. After playing a bit around, you see that it breaks all the code and its features. And you discard all of your changes and you say “well…maybe next time” because you are scared of breaking all the code and dependencies.
Another thing that developers fear, is that they won’t have time to write the tests. And that development time will be divided in half — half for writing the test and the other half for developing the features. This is completely untrue and we will be discussing techniques that prove this wrong.
Let’s use test-driven development for implementing our rules. By using test-driven development we can do two things:
- We will be sure that our game will work without the need of implementing any UI
- We can always come back to refactor the code, make it cleaner and more readable by making sure that we did not break anything
Test-driven development is lots of times misunderstood often by thinking that:
- Firstly we need to write all of the code for a feature and then start writing the test
- We need to write all the tests and then start writing the actual code
This is wrong and can bring the following thoughts and leave the test often out
- After you write the code for a feature and that code works, people often don’t go back to write tests for it
- Writing all the tests first takes a lot of time and features can be delayed because of that
The thing is that TTD is coding style and you write tests and code in a parallel manner by improving one another. We will explain this using the following technique.
Using the red-green-refactor technique
The red-green-refactor is a great technique for writing test together with coding. Simply by following three steps :
- Write a test that fails, that means write a test and let it fail. Don’t worry about the implementation
- Write the minimum required code for passing the test. That means don’t worry about the cleanest code for the function. Just write some code that passes the test
- Refactoring. Now, this is the best part because you can refactor your function confidently by making sure that your not breaking anything. Since the test will notify you about that
First, let's try to write a test for the survival rule:
“A square that is already alive can continue living to the next generation if it has two or three neighbors”:
If we run our test it will fail since our state(…) function currently returns a hardcoded boolean of “false” this is actually ok since we are respecting the rule of red-green-refactor technique.
Let's try to write the code for passing this test:
Here we go for all combination of the neighbors from x-1…x+1 with y-1…y+1 to count the left, right, up, down and the diagonal alive neighbors.
All of our tests now are passing. We can now easily refactor our function at any time because we have the test that we can trust. For example, we can decide in the future to use a reduce function instead of the for loops and this can be easily implemented because of the tests that we wrote. Whatever we do, we will make sure that our test will be running and we will know for sure that we didn’t break anything.
Our tests for the following two rules will be written in the same way as before. We’ll use the red-green-refactor technique and make sure that is not breaking any of the other tests.
After going back and forth with the test and the code, we finally came to the following tests and implementation of our state function:
We now have all of the rules of the game covered with tests! No matter how much we refactor, chop the functions down or add new functions, we can ensure that we didn't break our main rules. All this — by simply running the tests and see if all of them pass.
Creating our user interface
View part of the game will be created simply by a collection view that will be updated with the tick from a timer. It can be improved using game development frameworks for iOS but that may be included in a future part of this article.
We need to somehow observe the state of our game, so we are going to create some kind of a state observer.
Let’s add a state observer to our game :
And whenever our state changes, we will simply be reloading our data in our collection view:
And our cell configuration will look as following:
In configure cell, we simply set background either black or white (either dead or alive).
Seeing it in action
🙌 Ready to see the result?
If we run our game we can see something like the following:
A small universe of its own created from only three simple rules. If you want to get the complete project you can find it from here.
Studying the patterns created
If we keep observing the Game of Life, we’ll start seeing a lot of patterns in it.
There are similar objects that get created and live in a static manner without changing through the generations. There are some oscillating objects that change periodically according to a specific scheme, after a fixed number of generations, they return to their original state.
There are also patterns that are called gliders. They develop over time and they crawl along the array. They transform into different shapes until they get back to their initial shape but one square down diagonally.
It is interesting that physically we have only alive and dead squares on the small scale. But on a larger scale, we have blinkers, oscillators, spaceships, and different life blocks.
In our initial rules, we don’t have rules about moving nor colliding between objects. However, we can see that in the game there are laws, that can be observed for moving and collision.
If we define living beings as a system that can reproduce themselves. These objects can reproduce but not in a stable manner.
But what if we can imagine a more complicated set of laws that would allow a complex system with all the attributes of life?
Would such object be aware of itself and would it be self-conscious?
In this article, we have implemented the Game of Life using Swift in iOS. We implemented the rules of the game using Test-driven development and explained how TTD can make our development process better.
We also observed the patterns and discussed some philosophy behind this game.
Conway wanted to know if a universe with fundamental rules as simple as the ones of the Game of Life, could contain objects complex enough to replicate. Not only this got demonstrated that this is possible, but this game even showed that such an object would be, in a sense, intelligent!
If you enjoyed this article make sure to clap to show your support.
Follow me to view many more articles that can take your iOS Developer skills to the next level. If you have any questions or comments feel free to leave a note here or email me at firstname.lastname@example.org.
- Robert Martin Clean Code: Refactoring, Patterns, Testing, and Clean Code Techniques
- Stephen Hawking The Grand Design