Duncan Mackenzie
One of the things computers do best is to automate repetitive tasks, especially if those tasks involve a large set of numbers or other items. Usually, this leads to the use of computers to handle financial data, statistical analysis and related concepts, but sometimes the perfect sample for computer programmers can be a lot more fun.
Back in 1970, John Conway published a set of rules for a cellular automaton (defined as “a grid of cells that evolves according to a set of rules based on the states of surrounding cells” on Eric Weisstein’s World of Mathematics web site) that became known as the “Game of Life”. These rules were quite simple, based on a grid of cells that were either alive or dead (on/off if you like), the state of a cell could change every time the game moved ahead another generation according to three rules;
1. If the # of neighbors < 2 (loneliness) or > 3 (overpopulation) the cell will be turned off if it is currently on. If it is already off, nothing happens.
2. If there are exactly 2 or 3 neighbors, and the cell is on, it stays on.
3. If there are exactly 3 neighbors, and the cell is off, it is turned on.
Following these rules by hand, every generation could take sometime to calculate, but computers are perfectly suited to doing this for you. In this article, I will walk you through the creation of a Visual Basic .NET version of this game. If you have the book “Teach Yourself Visual Basic.NET in 21 Days” then consider this Bonus Project #1, as it builds on the knowledge you learned in the first 7 days worth of material, including variables, arrays, and loops. If you don’t have that book, then you may need another reference for those concepts, as I will not be explaining them in any detail in this article.
Before we jump into coding, we should always lay out what our program needs to do (the requirements) and determine at least a plan for handling each necessary step (the design). In this case, let’s start by describing what the program will do;
Core to our program, we will need the ability to maintain the current state of a grid (the on/off state of the cells on the grid that is) in memory. Next, we need to be able to populate our grid with a starting configuration of cells. It would be great to be able to load this starting configuration in from a file, but we’ll settle for a randomly generated set of cells for now. Then, we will need to take our current grid and advance to the next “generation”, based on John Conway’s rules, described above, which will involve calculating the # of neighbors for any given cell. We should be able to choose how many generations we run our simulation for as well, so this will need to be a user option.
With this description, we can create a description of our system in what some would call pseudo-code, but I think that the term ‘description’ works quite well at this point.
Program starts, accepting as input the # of generations to run (n)
Generates starting set of cells, using random number generator
Advances from the starting set to the next generation n times. This involves making a copy of the current grid and then processing each cell in the current grid by checking the # of neighbors and changing the state in the new (copy) grid accordingly.
Could output each generation, or only the last one… requirements didn’t specify, so this is up to us.
Done
With this rough plan in place, coding can start, at least in pieces.
This program could be created as a Console application or a Windows application; the game logic would remain the same in either case. I am going to choose to build a console application, because there is less unrelated code to confuse the issue. So, using the Visual Studio IDE, you will need to create a new “Console Application” project. This new project will automatically include a new empty module, which you should rename to something meaningful (not that Module1 isn’t meaningful to someone) like “Life” before you write any code. Another good preparation step is to add two lines to the top of your module specifying that you will not allow undeclared variables and that all data type conversions (from integer to string, for instance) must be explicit.
Option Strict On
Option Explicit On
Writing VB code without these settings can allow a lot of bugs to appear simply because you have told the compiler not to check for these problems. Now, with the basic preparation done, you can start to code. It isn’t time to write the entire program yet though, just some pieces. Looking at our requirements, and the resulting description of our system, several technical points are raised that can be individually addressed, starting with the in-memory representation of our game’s grid.
An array with two dimensions seems like the perfect choice to represent our grid, because it can be easily manipulated, allowing us to directly address the cell we want to work. Each individual cell has only two states (alive or dead) so we can use the Boolean data type to represent each cell. To declare our array, we will need to know the size of our grid and we will want to be able to change this value without too much effort so for now we will make it a Constant.
Private Const GridSize As Integer = 40
Private CurrentState(GridSize, GridSize) As Boolean
Keep in mind that arrays in .NET are zero-based, which means that the first element is myArray(0), and this is not a configurable option like it was in previous versions of Visual Basic. If the array was declared in C#, then there would be GridSize elements, so 0 to GridSize – 1, but in VB.NET a change was made to make migration from earlier versions easier. Our array ends up containing elements from 0 to GridSize (GridSize + 1 total elements), a noteworthy difference, and one that ends up producing a 41 by 41 grid for our game. If an even number was desired, you could make the GridSize constant 39, or declare the array as CurrentState(GridSize-1,GridSize-1), whichever you wish. Since we have made our array size easy to change, from this point on we will either dynamically get the array size or use the GridSize constant, so that all of our code works regardless of the grid size you choose.
In the .NET Framework, there is a special class just for generating random numbers, System.Random. By creating a new instance of this class, without specifying any constructor arguments, a new random number generator is created using the current time as a seed. To obtain a random value, you call the NextDouble method on the Random class, which returns a value between 0.0 and 1.0. So, to populate our grid, we could loop through each and every array element, and use the random number generator to decide if that element should start out alive or dead. Once we have a random number for a particular cell, we need to determine the approximate % of live cells that we want to have in our grid. If we were to test against 0.5, 50% of our cells would be alive, and 50% would be dead, but it may be interesting to be able to control this distribution. Let’s create a constant that represents the desired % of living cells, and for each cell we will compare our random value against that constant. If the random number is less than the constant, then the cell will be alive (element = True), if it equal to or greater, the cell will be dead (element = False). The code to accomplish this would look something like the following:
Private Const lifeDistribution As Double = 0.3
Dim i, j As Integer
Dim numGenerator As System.Random
For i = 0 To GridSize
For j = 0 To GridSize
If numGenerator.NextDouble < lifeDistribution Then
CurrentState(i, j) = True
Else
CurrentState(i, j) = False
End If
Next
Next
To make our code readable, and easily maintained, it is best to break this out into a procedure, one that accepts the grid as an argument. We will make the grid an argument that is passed by reference (ByRef) so that our code will be working directly on the grid in the main program.
Private Sub PopulateStartingGrid(ByRef grid(,) As Boolean)
Dim i, j As Integer
Dim numGenerator As System.Random
For i = 0 To GridSize
For j = 0 To GridSize
If numGenerator.NextDouble < lifeDistribution Then
grid(i, j) = True
Else
grid(i, j) = False
End If
Next
Next
End Sub
In our main program, we can then populate our grid by calling PopulateStartingGrid(CurrentState).
Given a specific element address, such as (3, 9), we need to be able to count the number of live neighbors. This can be accomplished by looking at the eight (or less, if the element in question is near an edge) elements that touch upon the specific cell.
If we can access an element with this syntax CurrentState(3,9), then we can build a function to count neighbors in several different ways. The most straightforward way is to build the function using all if statements, but we could also use a loop or a loop with some creative error handling. Here is the first way this function could be written, using just if statements:
Private Function CountNeighbors(ByRef grid(,) As Boolean, _
ByVal cellX As Integer, _
ByVal cellY As Integer) As Integer
Dim count As Integer
count = 0
If cellX > 0 And cellY > 0 Then
'if both are > 0 then I can look at
'upper-left, upper, and left cells safely
If grid(cellX - 1, cellY - 1) Then count += 1
If grid(cellX, cellY - 1) Then count += 1
If grid(cellX - 1, cellY) Then count += 1
End If
If cellX < GridSize And cellY < GridSize Then
'if both are < GridSize then I can look at
'lower-right, right, and lower cells safely
If grid(cellX + 1, cellY + 1) Then count += 1
If grid(cellX, cellY + 1) Then count += 1
If grid(cellX + 1, cellY) Then count += 1
End If
If cellX > 0 And cellY < GridSize Then
If grid(cellX - 1, cellY + 1) Then count += 1
End If
If cellX < GridSize And cellY > 0 Then
If grid(cellX + 1, cellY - 1) Then count += 1
End If
Return count
End Function
I won’t build the function the other two possible ways, but you might want to experiment with these other options and compare the performance of each.
With our neighbor counting routine completed, calculating the next generation should be relatively easy. We will need another array, of the same size as the first, to store our new generation. The new state will be placed into this second array as we calculate it by looping through all the elements of the current state. As before, we will separate this code into a function, passing in the current state by reference to avoid making a duplicate in memory and returning a new array representing the next generation.
Private Function CalculateNextGeneration( _
ByRef currentState(,) As Boolean) As Boolean(,)
Dim nextGen(GridSize, GridSize) As Boolean
Dim i, j As Integer
Dim neighbors As Integer
For i = 0 To GridSize
For j = 0 To GridSize
neighbors = CountNeighbors(currentState, i, j)
If neighbors = 2 Or neighbors = 3 Then
If neighbors = 2 Then
nextGen(i, j) = currentState(i, j)
Else
nextGen(i, j) = True
End If
Else
nextGen(i, j) = False
End If
Next
Next
Return nextGen
End Function
A simple loop and the Console.Write and Writeline commands are sufficient to make a printing routine that accepts an array and outputs to the console.
Private Sub PrintGrid(ByRef grid(,) As Boolean)
Dim i, j As Integer
For i = 0 To GridSize
Console.Write("*")
Next
Console.WriteLine()
Console.WriteLine()
For i = 0 To GridSize
For j = 0 To GridSize
If grid(i, j) Then
Console.Write("X")
Else
Console.Write(" ")
End If
Next
Console.WriteLine()
Next
End Sub
Now, we have almost everything we need, so we will add one more feature as a finishing touch and then create our main program by combining all of these individual items.
Back in our requirements, we stated that it would be nice if we could specify the number of generations to compute by providing a command line argument. Well, in Visual Basic†, the command line arguments submitted to your Console application are available through the System.Environment.GetCommandLineArgs() method. This method returns an array of strings, with the first element being the name of your executable and subsequent elements contain any command line arguments passed to your program.
†Command line arguments are available in this fashion to C# programmers as well, but in C# the command line arguments are also passed as a parameter into your Main subroutine.
To loop through all of the arguments passed to your program, looking for a specific one can be achieved through code like this:
Private Function GetNumberOfGenerations() As Integer
Dim args() As String
Dim arg As String
args = System.Environment.GetCommandLineArgs()
For Each arg In args
If arg.StartsWith("/g:") Then
Return ParseGenSwitch(arg)
End If
Next
End Function
Private Function ParseGenSwitch(ByVal switch As String) As Integer
Dim tmp As String
tmp = switch.Substring(3)
Try
Return Integer.Parse(tmp)
Catch e As Exception
Return -1
End Try
End Function
In this case, we have made our code return a -1 if no command line switch is found, back in our mainline we will check for this value to see if the user submitted a desired number of generations.
With all of individual bits of functionality written as procedures, finishing the game is the easiest part of this whole project. Here is the complete code, including all the procedures and the main routine:
Option Strict On
Option Explicit On
Module Life
Private Const GridSize As Integer = 40
Private CurrentState(GridSize, GridSize) As Boolean
Private Const lifeDistribution As Double = 0.3
Private Const defaultGenerations As Integer = 10
Private Const genSwitch As String = "/g:"
Sub Main()
Dim i, numGenerations As Integer
numGenerations = GetNumberOfGenerations()
If numGenerations < 1 Then
'didn't supply Gen Switch, or supplied < 1
Console.WriteLine( _
"Use {0} to indicate desired number of generations", _
genSwitch)
Console.WriteLine( _
" for example GameOfLife.exe {0}3", _
genSwitch)
Console.WriteLine( _
"Generations must be equal to or greater than 1")
Console.WriteLine()
Console.WriteLine( _
"{0} will be used as the default number of generations", _
defaultGenerations)
numGenerations = defaultGenerations
End If
PopulateStartingGrid(CurrentState)
For i = 1 To numGenerations
CurrentState = CalculateNextGeneration(CurrentState)
Console.WriteLine("Generation {0}", i)
PrintGrid(CurrentState)
Next
Console.WriteLine("Game of Life Completed")
Console.ReadLine()
End Sub
Private Sub PopulateStartingGrid(ByRef grid(,) As Boolean)
Dim i, j As Integer
Dim numGenerator As New System.Random()
For i = 0 To GridSize
For j = 0 To GridSize
If numGenerator.NextDouble < lifeDistribution Then
grid(i, j) = True
Else
grid(i, j) = False
End If
Next
Next
End Sub
Private Function CountNeighbors(ByRef grid(,) As Boolean, _
ByVal cellX As Integer, _
ByVal cellY As Integer) As Integer
Dim count As Integer
count = 0
If cellX > 0 And cellY > 0 Then
'if both are > 0 then I can look at
'upper-left, upper, and left cells safely
If grid(cellX - 1, cellY - 1) Then count += 1
If grid(cellX, cellY - 1) Then count += 1
If grid(cellX - 1, cellY) Then count += 1
End If
If cellX < GridSize And cellY < GridSize Then
'if both are < GridSize then I can look at
'lower-right, right, and lower cells safely
If grid(cellX + 1, cellY + 1) Then count += 1
If grid(cellX, cellY + 1) Then count += 1
If grid(cellX + 1, cellY) Then count += 1
End If
If cellX > 0 And cellY < GridSize Then
If grid(cellX - 1, cellY + 1) Then count += 1
End If
If cellX < GridSize And cellY > 0 Then
If grid(cellX + 1, cellY - 1) Then count += 1
End If
Return count
End Function
Private Function CalculateNextGeneration( _
ByRef currentState(,) As Boolean) As Boolean(,)
Dim nextGen(GridSize, GridSize) As Boolean
Dim i, j As Integer
Dim neighbors As Integer
For i = 0 To GridSize
For j = 0 To GridSize
neighbors = CountNeighbors(currentState, i, j)
If neighbors = 2 Or neighbors = 3 Then
If neighbors = 2 Then
nextGen(i, j) = currentState(i, j)
Else
nextGen(i, j) = True
End If
Else
nextGen(i, j) = False
End If
Next
Next
Return nextGen
End Function
Private Sub PrintGrid(ByRef grid(,) As Boolean)
Dim i, j As Integer
Console.WriteLine()
For i = 0 To GridSize
Console.Write("*")
Next
Console.WriteLine()
For i = 0 To GridSize
For j = 0 To GridSize
If grid(i, j) Then
Console.Write("X")
Else
Console.Write(" ")
End If
Next
Console.WriteLine()
Next
End Sub
Private Function GetNumberOfGenerations() As Integer
Dim args() As String
Dim arg As String
args = System.Environment.GetCommandLineArgs()
For Each arg In args
If arg.StartsWith(genSwitch) Then
Return ParseGenSwitch(arg)
End If
Next
End Function
Private Function ParseGenSwitch(ByVal switch As String) As Integer
Dim tmp As String
tmp = switch.Substring(genSwitch.Length)
Try
Return Integer.Parse(tmp)
Catch e As Exception
Return -1
End Try
End Function
End Module
Notice that in the final version a few things have been changed, including replacing some hard coded values with constants. This is an important process, looking through your code for any value that has been specified directly in your code and should have instead been a constant or a variable. It is critical that you look carefully though, as some values will be used in many places. In this example, consider the command line switch for the number of generations ("/g:"). To replace this switch with a constant, a variety of lines had to be changed, as highlighted in red below;
Sub Main()
Dim i, numGenerations As Integer
numGenerations = GetNumberOfGenerations()
If numGenerations < 1 Then
'didn't supply Gen Switch, or supplied < 1
Console.WriteLine( _
"Use {0} to indicate desired number of generations", _
genSwitch)
Console.WriteLine( _
" for example GameOfLife.exe {0}3", _
genSwitch)
Console.WriteLine( _
"Generations must be equal to or greater than 1")
Console.WriteLine()
Console.WriteLine( _
"{0} will be used as the default number of generations", _
defaultGenerations)
numGenerations = defaultGenerations
End If
…
End Sub
Private Function GetNumberOfGenerations() As Integer
Dim args() As String
Dim arg As String
args = System.Environment.GetCommandLineArgs()
For Each arg In args
If arg.StartsWith(genSwitch) Then
Return ParseGenSwitch(arg)
End If
Next
End Function
Private Function ParseGenSwitch(ByVal switch As String) As Integer
Dim tmp As String
tmp = switch.Substring(genSwitch.Length)
Try
Return Integer.Parse(tmp)
Catch e As Exception
Return -1
End Try
End Function
Although our program is done, there are always more features that could be added. A few that I could suggest, and that you may wish to try on your own, are listed here:
Make the GridSize configurable through a command line switch, like the number of generations is.
Allow the final generation to be saved to a file (an XML file, for instance)
Add a command line parameter to allow the user to specify a filename to save the final generation to.
Allow the starting configuration to be loaded from a previously saved generation
And finally, a really big idea… Change this application from a command line program to one with graphical output, built around a Windows Forms interface.
The Game of Life is an excellent program to write to give you a chance to play with Visual Basic .NET, especially with arrays and loops. Build this example, and then add whatever features you wish, you might even have some feature ideas that I didn’t think of! If you are looking for more information on the topics discussed in this article, check out this list of references:
Arrays and other variables: Day 3 of Teach Yourself Visual Basic.NET in 21 Days, Introduction to Programming with Visual Basic .NET, page 77
If statements and Loops: Day 4 of Teach Yourself Visual Basic.NET in 21 Days, Controlling Flow in Programs, page 99
The Game of Life: Eric Weisstein’s World of Mathematics web site (http://mathworld.wolfram.com/Life.html)