Channels ▼

Clay Breshears

Dr. Dobb's Bloggers

Game of Life — Using Lists to Focus Processing

March 19, 2013

In this post, I'm going to discuss the basic parts to code the Game of Life using list data structure to hold the sets of grid points that may become alive or may die in the next generation. This modification can reduce the amount of processing needed by focusing the computation on the volatile areas of the grid and leaving those parts that are empty or that have achieved a stable configuration of cells. This approach and algorithm comes from the book Data Structures & Program Design in C (Second Edition) by Robert Kruse, C.L. Tondo, and Bruce Leung (Prentice-Hall, 1997), which I'll shorthand as DSPDC.

As I said in my last post, this variation needs two grids: one to hold the status and position of live cells in the current generation and one to hold the count of the neighbors for corresponding cells. In addition, the algorithm requires four lists of grid points. The global declarations for the code are below.

Grid Map;                // array holding cells
Gridcount numNeighbors;  // array holding neighbor counts
List newlive,            // cells that have just been vivified
     newdie,             // cells that have just died
     maylive,            // candidates to vivify in next generation
     maydie;             // candidates to kill in next generation
int maxrow, maxcol;      // user defined grid size

My user-defined data type Grid and Gridcount are simply two-dimensional arrays. Elements in the Map array hold one of two enumerated states (DEAD, ALIVE) and the numNeighbors array holds integers. The dimensions of the arrays are both maxrow+2 by maxcol+2.

The List type is an abstract data type (ADT) implementing a simple list structure. DSPDC is teaching abstract data types and implements different versions of the List type. Keeping with the spirit of ADTs, I won't give out the implementation details for the List type (and leave that as an exercise to the reader). Each list holds elements (of type Cell) that contain two integers: the row and column index of a notable grid point. The operations defined for the List structure can create a new empty list (CreateList), clear the list of all contents (ClearList), check if the list is empty (ListEmpty) or full (ListFull), return the number of items on the list (ListSize), add an item to the list (AddList), pass through the list and execute a given function on each item (TraverseList), and make a copy of the list onto another list (CopyList).

The main() routine first initializes the Map and numNeighbors grids from an input file (assumed to be given on the command line) of initial live cells. Each of the four lists is also initialized. The WriteMap() routine prints out the current configuration of live cells. (I originally had a call to WriteMap() in the body of the loop to watch the progress of the cells on the grid, but I’ve taken it out for simplicity.)

   Initialize(map, numNeighbors, &newlive, &newdie, &maylive, &maydie, argv[1]);
   WriteMap(map,gencount);
   while (gencount < GENS) {
         gencount++;
         TraverseList(&maylive, Vivify);
         TraverseList(&maydie, Kill);
         ClearList(&maylive);
         ClearList(&maydie);
         TraverseList(&newlive, AddNeighbors);
         TraverseList(&newdie, SubtractNeighbors);
         ClearList(&newlive);
         ClearList(&newdie);
      }
   }

Once the initial set up is done, a while loop runs over the computation steps for each new generation until the target number of generations (GENS) has been computed. To explain the processing required, I like to think that the computation involved in determining the status of the current generation has two phases. The first phase traverses the list of possible new cells (maylive) and possible cells that will be dead (maydie). For each element in the lists, the number of neighbors of the cell is consulted and the appropriate action (new live cell or kill overcrowded cell) is taken. The two lists are cleared in preparation of the next generation. (Note: I'm using lists and not stacks or queues, so the traversal of a list will not remove or change any elements, which is why the separate list clearing function is needed).

For the second phase, the other two lists are then traversed. This is the list for newly live cells (newlive) and those grid points that just had a live cell die (newdie) as found in the first phase. The traversal of these lists updates the numNeighbor counts and determines if an updated count points toward a cell that should become alive or dead in the first phase of the next generation computation. The two traversed lists are cleared at the end of the second phase.

So far I've kept the details of the code at a high level and only hinted at how items are added to the respective lists. Before I get into the details of the computation and functions used to process each list item during a list traversal, ensure that you understand the general idea behind the main generation computation loop.

(In the years that I've been writing code, I have found that assuming your functions work as they are intended is a basic principle of my program design process. Once you have the functionality well defined, you can write code that calls those functions without knowing the details of how the function is implemented. This is the fundamental reason ADTs are so useful. It doesn’t matter if your list is implemented as a statically allocated or linked structure, or an expandable structure, as long as the interfaces perform as defined.)

The Initialize() routine, after initializing each of the four new lists, reads in the locations of each live cell of the starting state. The size of the grids can be read in as part of the input file or preset to some fixed values. Each valid cell that is contained in the input file is added to the newlive list and marked as ALIVE on the Map array. This list is traversed with the AddNeighbors() function to set the initial values for the numNeighbors. Finally, the contents of the newlive list are copied into the maydie list (and the newlive list is cleared) since any one of the initial live cells may have too many neighbors to survive into the first computed generation.

Because AddNeighbors() is used to initialize the numNeighbors array with the proper starting values of the starting cell configuration, you must ensure that there are no duplicate cells in the newlive list. (I'll deal with the potential hazard of duplicate cell entries on lists during the generation computation later in this post.) As you'll be able to see from the AddNeighbors() code below, to get an accurate initial count of the neighbors, I need to watch the file input to remove duplicates or grid points outside of the range of the boundaries.

My code for this version of Game of Life is written in C. Parameters of functions in C can themselves be functions. This feature allows me to write a single TraverseList() function that can apply a different computation to items on the list depending on the function given as the second parameter. My implementation of TraverseList() simply walks down the list and calls the given function on each element of the list. If you use a predefined list structure (say, one that is thread-safe, nudge nudge) and there is no equivalent traversal functionality, you might need to write a "meta" traversal function that can apply the desired function to each element of the list.

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 


Video