Writing the Game of Life in VB.NET

Bonus Project 1 – Teach Yourself VB .NET in 21 Days

Duncan Mackenzie

Introduction

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.

 

Planning our Program

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.

 

Starting to Code

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.

 

The Game 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.

 

Creating a random starting configuration

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).

 

Counting Neighbors

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.

 

Making the Next Generation

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

 

Printing a Generation

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.

Accepting Command Line Arguments

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.

 

The Complete Game

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

 

 

Adding Extra Features

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:

 

Conclusion

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: