Cornpa Educ. Vol. 10, No. 2, pp. 315-325, 1986 Printed in Great Britain. All rights reserved
Copyright 0
ON COUNTING
IN THE GAME OF LIFE
CLIFTON FINNEY School of Sciences and Technologies, University of Houston-Clear Houston, TX 77058, U.S.A. (Received
0360-1315/86 53.00+0.00 1986 Pergamon Press Ltd
Lake, 2700 Bay Area Boulevard,
1 July 1985; revision received 26 Augusr 1985)
Abstract-A new, indirect method of counting neighboring cells for Conway’s “Game of Life” is described. Implementation requires one data structure, and runtime depends upon the sparseness of the infinite array. Student response to the programming project was enthusiastic and the population that used indirect counting produced highly efficient programs.
INTRODUCTION
Pascal was invented “to make available a language suitable to teach programming as a systematic discipline . . . “[l]. That the structured programming aim was on target is now everywhere evidenced in college and university textbooks, and even in the content of new, more sophisticated languages such as Ada[2]. Yet, that is not all there is to the teaching of programming. Behind an efficient program is efficient counting. This classical precept was once again demonstrated by the performance of students in a course on Structured Program Design and Coding where a new, indirect method of counting neighboring cells for Conway’s “Game of Life”[3] was presented. “Life” first gained popularity after Gardner introduced it to a wide audience in his column[4]. Since then, others[5] have helped maintain interest with followup reports on technical developments and their potential applied significance. The game has also appeared as an application in at least three programming textbooks[6-81, the last of which, is one of two used by us in the course. The other text[9] we use is a thorough, readable account of standard Pascal. The game is setup on a two-dimensional unbounded grid of adjacent squares so that each square has eight neighboring squares. Each square may contain either a living or a dead cell. The programmer (user) supplies an initial configuration of living cells. The next generation of living cells at time t + 1 is computed from the present generation at time t according to two rules: (1) (2)
If a cell is alive, and it has two or three neighbors alive, it is also alive in the next generation. If a cell is dead, and it has three neighbors alive, it becomes alive in the next generation.
Thirty-one beginning graduate students in two sections were assigned and completed the problem. These students all have undergraduate degrees in fields other than computer science and approximately two-thirds are employed in the large oil and petrochemical, medical, and aerospace complexes that surround the campus which is located directly adjacent to the Johnson Space Center. The remaining third of the population consisted of full-time students. The course is a simple linear combination of two undergraduate offerings-Structured Programming with Pascal and Structured Software Design-and is intended as one course in a sequence of three to quickly and efficiently build competence to the graduate level. DIRECT
COUNTING
AND
DESIGN
As seen schematically in the left path of Fig. 1 the question asked for direct counting is: How many living neighbors are there for each cell in the theoretically infinite two-dimensional grid? This leads quite naturally to an algorithm of the form: Begin Algorithm I Initialize Read rhe Read rhe setting
LIFEARRA Y to DEAD and NEIGHBORARRAY number of generations lo be computed sets of coordinates for the zerorh generation appropriate elements of LIFEARRA Y to ALIVE
IO zero
315
CLIFTON FINNEY
316
Call WRITEM to write the initial list in LIFEARRA Y For each generation do For each element do Set NEIGHBORARRAY[I,J] to NEIGHBORCOUNT[I,J] End do For each element do I/(NEIGHBORARRAY[I,J] = 3) or ((NEIGHBORARRAY[I,J] and (LIFEARRA Y[I,J] = ALIVE)) then Set LIFEARRAY[I,J] to ALIVE Else Set LIFEARRAY[I,J] to DEAD End if End do Call WRITEM to write the new generation from LIFEARRAY End do End
= 2)
LIFEARRAY is a two-dimensional enumerated array whose elements contain the information whether each cell is ALIVE or DEAD. NEIGHBORARRAY is a two-dimensional integer array whose elements contain the number of living neighbors for each element in the present generation of LIFEARRAY. NEIGHBORCOUNT is an integer function which takes the rows and columns of LIFEARRAY for parameters, counts the number of surrounding neighbors that are alive and returns the integer count. Beyond being artificially bound in size to the dimensions of the array, this algorithm, based as it is on direct counting, is inefficient since most of the elements of LIFEARRAY will always be
Fig. 1. Direct counting
in the left path and indirect counting in the right path of living neighbors. circles represent living cells.
Open
Counting in the game of life
317
Fig. 2. More detailed representation of the steps involved in the indirect counting process for the right path of Fig. 1. Again, opem circles represent living cells, and the heavy lines form an outline of the neighboring cells which are being counted in each of the steps.
DEAD. That is, LIFEARRAY is a sparse matrix and if, for example, it were dimensioned 25 x 80 so as to be suitable for viewing on the screen of a microcomputer in text mode, then each generation would require upwards of 26,000 assignment statements for execution (8000 for assignments and counted do loops in the algorithm above and 18,000 in NEIGHBORCOUNT). INDIRECT
COUNTING
AND DESIGN
As illustrated in the right path of Fig. 1, the question asked for indirect counting is: How many other living neighbors does each cell of a living neighbor possess? It is seen that the question somehow
leads to an automatic confinement of the counting process to the immediate neighbors of living CellS.
Figure 2 illustrates in detail how the confinement automatically occurs. Figure 2a shows that living cell to the left has one living and seven dead neighbors. Each of the neighbors now has one living cell for a neighbor as counted. Figure 2b shows that the living cell in the middle has two living and six dead neighbors. Since four of these were also neighboring cells for the living neighbor to the left, their counts are increased to two while the counts to for the remaining cells are increased to one. Finally, Fig. 2c demonstrates that the living cell to the right has one living and seven dead neighbors. The mid-top and mid-bottom positions are neighbors to all three living cells so their counts are increased to three. The counts of the living cell in the middle, together with the positions
CLIFTONFINNEY
318
above and the below the living cell to the right are increased to their final values of two. The counts for the three neighbors to the right are set to one to complete the counting. A limited, special solution to the infinite array problem using indirect counting might also employ two arrays and be of the form: Begin AIgorithm II Read the number of generations to be computed READ the sets of coordinates for the zeroth generation into LIVINGARRAY by row-column order with LLIMIT Call WRITEM to write the zeroth generation For each generation do Set NLIMIT co zero For each set of coordinates in LIYINGARRA Y do C&l GENERATE IO set the COORDINATE, STATUS, AND COUNT fie& of NEIGHBORINGARRAY End do For each set of coordinates in NEIGHBORINGARRAY do If (COUNT = 3) or ((COUNT = 2) and (STATUS = ALIVE)) then Co~pte coordinates to LIVINGARRAY with LLIMIT as End $ End do Sort the sets of coordinates in LIVINGARRA
Y by row-column order Call WRITE.44 to write the new generation from LIVINGARRA Y End do End
Here, LLIMIT and NLIMIT are integer indices used to delimit the active portions of LIVINGARRAY and NEIGHBORINGARRAY, respectively. LIVINGARRAY, is an n x 2 integer array where n is some arbitrary upper limit of living cells for the special case under consideration. For the display screen of a microcomputer indicated above, n would assume a value of 2000. The first column of LIVINGARRAY would contain values for the row coordinates and the second column would contain the values for the column coordinates of living cells. It is again emphasized that the coordinates contained in LIVINGARRAY are “conceptual coordinates*’ of the conceptual map being read from or written to, and not the map itself as above in the case for direct counting. NEIGHBORINGARRAY is an n x 4 array whose first two columns also contain the integer values for coordinates, but this time of the neighboring cells of each living cell in LIVINGARRAY as created and inserted by the procedure GENERATE. The third column of NEIGHBORINGARRAY contains the status, dead or living, and the fourth column contains the number of occurrences or counts of that particular neighbor. Thus, the procedure GENERATE given in APPENDIX A is moderately sophisticated. It first takes each set of coordinates in LIVINGARRAY and generates the coordinates of the eight neighbors. For each neighbor that it generates, it searches NEIGHBORINGARRAY to determine if the coordinates are already present, and if they are, it increases the count field. If the coordinates are absent, then it adds them to the end of NEIGHBORINGARRAY together with the status (after it has checked LIVINGARRAY to determine that status) together with a count of one. An advantage of this algorithm is that it is unnecessary to ever have to initialize or re-initialize arrays. Working from the top to bottom for each generation in the arrays with LLIMIT and NLIMIT, it is only necessary to keep track of how many rows are currently active. It would also be possible to design the algorithm around a single array similar to NEIGHBORINGARRAY, but only at the potential expense of increased runtimes due to longer searches and sorts. As it is, Algorithm II was also time-consuming of order approximating nz since most students were inclined to use either bubble or insertion sorts; however, the lists in their test case were very short. It is fully appreciated that a variety of improvements in searching and sorting for Algorithm II might be suggested and made. However, since this entire approach with fixed arrays fails to the address the unbounded grid problem posed initially by Conway, it is felt that the underlying value of Algorithm II rests elsewhere in introducing indirect counting and the manipulation sparse conceptual arrays.
Counting in the game of life
319
A somewhat more general solution of the infinite array problem with indirect counting would employ the following PASCAL type definitions and a linked list of records as follows:
TYPE ETA l-US RANGE CELLPOINTER CELLNODE
= (DEAD, ALIVE); = C&.8; = h CELLNODE; = RECORD ROW COLUMN STATE COUNT NEXT END;
: : : : :
INTEGER; INTEGER; STATUS; RANGE; CELLPOINTER
Begin Alogorithm III Call READEM lo read ihe number of generalions to be computed, to initialize the linked list of cellnodes, and to read, link, and set the fields of the zerofh generation of living cells Call WRITEM lo write the zeroth generation For each new generation do Call NEIGHBORS to search for living cells in the list When found, call ACTION to search for andfmd, or IOgenerate, link, and set fiela!s for all neighboring cellnodes. Call WRITEM to dispose of &ad nodes and IO reset and write living nodes in the new generation End do End
The call to the procedure READEM sets up a linked list of type CELLNODE with LISTPOINTER pointing to a dummy node as the head of the list and LASTPOINTER pointing to the node at its end, initially nil. It reads the number of generations to be processed followed by the coordinates for the zeroth generation in row-column ascending order. As it performs the latter task, it sets the ROW, COLUMN, STATE, COUNT and NEXT fields of new CELLNODES and links them to the end of the list. The procedure NEIGHBORS uses a pointer NOWALIVE to chain the list for cellnodes whose state is ALIVE. It also sets up the pointers, SECONDPOINTER and FIRSTPOINTER, to point to the dummy node and the first node of the list, respectively, in preparation for the first call to the procedure ACTION. After NOWALIVE has located a living cell in the list, then NEIGHBORS calls ACTION. ACTION, as given in APPENDIX B, takes the ROW and COLUMN fields of the node pointed to by NOWALIVE and generates the coordinates of the neighbors. One by one, as the sets of coordinates are generated, the list is traversed with SECONDPOINTER and FIRSTPOINTER until a cell with these coordinates is found, or until a cell whose coordinates exceeds these coordinates is found. In the former situation, the COUNT field is incremented; in the latter, an entirely new node is inserted into the list with that set of coordinates, with STATE equal DEAD, and with COUNT equal one. It is seen in Appendix B that to promote searching efficiency still another pointer, NEWSECOND~INTER, marks the place in the list where the initial search begins after a call from NEIGHBORS. Thus, after the search for the current neigh~~ng cells ends, FIRSTPO~NTER and SECONDPOINTER are reset in preparation for the next call from NEIGHBORS. The effect is to localize the list that is being searched so that rather than having order n2 for a list with n nodes, we have order nm with m the average length of the reduced list and m
CLIFTON FINNEY
320
Table I. Student results on assignment statements for direct and indtrect counting of neighbors Direct Student
Assianment 72. I56 78.847 92.626 101.161 114.024 127,905 142.1 IO 180.007 351,476 756.577 Av. 202,000
I
2 3 4 5 6 7 8 9 IO II I2 I3 I4 I5 lb I7 I8 19 20 21
Indrrcct statements 4180 5363 541 I IO.102 10.28 I Il.954 13.51 I 14.418 18,869 2 1,227 23.527 23,779 28.199 29,486 31,720 32,808 35.490 37.062 41,951 68,501 102,545 Av. 27,COO
Fig.
3. &r&t
through
seventh
generation
total
of test pattern of living assignment statements.
cells used to determine
runtimes
and
Counting in the game of life
321
RESULTS Table 1 presents the number of assignment statements of direct and indirect counting for programs implemented by the students using the zeroth through tenth generation of one test pattern as seen in Fig. 3. The pattern in Fig. 3 repeats itself after the seventh generation. The counts of assignment statements exclude all comparisons and procedure calls, but include even the assignments made in counted do loops. Most members of the class used the University’s VAX 1l-780 system [ lo], but they were free to use any reputable machine and compiler. In any case, they dimensioned their real arrays and wrote to conceptual arrays of 25 x 80 in text files. The challenge laid down was to write, document, and test an efficient program for the “Game of Life.” Each was also free to choose either direct or indirect counting. Most students who used direct counting designed their program along the lines of Algorithm I; most who used indirect counting designed along the lines of Algorithm II. Figure 4 illustrates data taken by the instructor on runtimes and counts of assignments statements to compare the performances of Algorithms I and III. The runs were made on an IBM PC Model One with a Turbo Pascal compiler[lO]. Assignment statements were counted as stated above. The runtimes excluded reading in the data and writing to the screen for the zeroth generation. Time measurements for the subsequent ten generations were made between two MsDOS function calls as provided for in Turbo Pascal. The time between these calls was only 0.055
40
30 t
0
X -
20
? 5 E
IO
0
75
: *
SC
: F 25 /
-
0 0 PATTERNS
Fig. 4. Instructor’scount of total assignmentstatements(top) and runtimes(bottom)for algorithmsbased on the first (solid circles) and third (x’s) program designs. The abscissacorrespondsto the number of nominallynon-interactingpatternsas shown in Fig. 3 that were included in the runs for ten generations.
322
CLIFTONFINNEY
seconds so that it was unneccessary to correct the runtimes which were much larger as seen in the figure. Arrays were also, actually or conceptually, 25 x 80. The abscissa in Fig. 4 corresponds to the number of non-interacting patterns of the type shown in Fig. 3 that were included for a run. A value of one means that one pattern was run, a value of two means that two were run simultaneously, etc. The patterns were made “non-interacting” by spacing them vertically and horizontally so that they would at least three units apart when, individually, they were in their widest extent in the seventh generation. Thus, the values along the abscissa in Fig. 4 are inversely proportional to the sparseness of cells on the screen. The average number of living cells over ten generations for one pattern was 10.9. The average number for 14 patterns was 152.6. These correspond to a range of 0.545-7.63 percent of the 2000 spaces on the screen being filled. DISCUSSION
The results in Table 1, by themselves, reflect neither scientific sampling methods nor scientific proof of the superiority of algorithms for indirect counting over algorithms for direct counting. Had, for example, the instructor approached the sampling problem more scientifically, he might have selected one section of students as control for direct counting, and another section as variable for indirect counting. As it was, class members in both sections had free choice as to counting method they used. It could be charged that the more capable students of programming chose the indirect counting method. As to direct counting, it is possible to build significant efficiencies into Algorithm I outlined above, and Kruse [8] has discussed these extensively. The text’s treatment of “Life” begins with a version that also scans the grid at each generation. The second version treats the grid as a sparse array using four lists and several procedures to locate and handle the elements that require attention from generation to generation. The last version retains the lists and procedures, but substitutes a chained hash table, linked lists and dynamic allocation for the main data structure. Examination of the better performing programs with direct counting indicated that some students had read the text. However, the results in Table 1 do seem to indicate that almost any student who wrote an algorithm based on indirect counting had a relatively easy task of producing an efficient outcome.
Fig. 5. Conway’s glider in (a) and Gosper’s glider gun in (b).
Counting in the game of life
323
This is not to suggest that the programming was necessarily easy. One student who produced one of the very lowest statement counts with indirect counting commented that she “wrote, rewrote and re-rewrote into the wee hours of the mom”. The nominal lines in Fig. 4 do help to provide insight regarding the ease with which the students who used indirect counting were able to write efficient programs. Here, there was a clear distinction between Algorithm I based on direct counting and on Algorithm III based upon indirect counting. It is seen in the figure that both the statement counts and runtimes for Algorithm I are relatively insensitive to the number of patterns. Conversely, both the statement counts and runtimes for Algorithm III are directly dependent upon the number of patterns, and similar results, but with greater rates of increase, would also be expected for Algorithm II. Beyond the worthwhile goal of promoting student interest and performance in classroom exercises, there may be some limited technical potential for the faster indirect counting algorithms described herein. Certain initial configurations termed “glider guns”, as seen in Fig. 5b, can be designed [3-51. These fire the smaller “gliders” in Fig. 5a diagonally across the infinite array. Configurations with several of the guns can be organized to simulate the behavior of logic gates and computers as well as collision-phenomena in particle physics and other cellular automata. Therefore, the possibility of speeding up some simulations, especially ones employing very sparse arrays, exists. CONCLUSIONS The results on assignment statements and runtimes indicate that indirect counting leads to more efficient algorithms for the “Game of Life.” Indirect counting may have some fortuitous technical implications relating to faster graphics similations of logic gates, computers, collision phenomena and cellular automata. Finally, it has not escaped our attention that several of the counts of assignment statements in Table 1 reported by the students were lower than the instructor’s own in Fig. 4. One lesson taken is that once the instructor has said something about structured programming, efficiency, documentation and counting maybe he should be satisfied that some students, at least, will give the very best. Another is that improvements in efficiency by future generations of students of the “Game of Life” based on indirect counting and Algorithm III are more than probable; they are likely. thank members of the class Nanci Anderson, Byron Brasseux, Ellen Crippen, Kristin Glywaski, Donna Holman and Mary Beth McMahan for sharing their thoughts on programming.
Acknowledgements-1
REFERENCES 1. Jensen K. and Wirth N., Pascal User Manual and Report, 2nd edn. Springer, New York (1974). 2. Ada is a registered trademark of the U.S. Government Joint Program O&e. 3. Berlekamp E. R., Conway J. H., and Guy R. K., Winning Waysfor Your Mathematical Plays. Academic Press, New York (1982). 4. Gardner M., Sci. Am. 223(4), 120-123 (1970); 224(2), 112-117 (1971). 5. Dewdney A. K. Sci. Am. U2(5), 18-30 (1985). 6. McGowan C. L. and Kelly J. R., Top-Down Srructured Programming Techniques. Petrocelli-Charter, New York (1975). 7. Barnard D. T. and Crawford R. G., Pascal Programming Problems and Applications. Prentice-Hall, Reston (1982). 8. Kruse R. L., Data Structures and Program Design. PrenticeHall, Englewood Cliffs (1984). 9. Leestma S. and Nyhoff L., Pascal Programming and Problem Solving. Macmillan, New York (1984). IO. VAX, IBM, and Turbo Pascal are trademarks of Digital Equipment, International Business Machines, and Borland International Corporations, respectively.
APPENDIX
A
As stated in the text, the procedure GENERATE takes a set of coordinates from LIVINGARRAY for parameters. These are passed as LIFEROW and LIFECOL. In this procedure the two for-loops generate to coordinates of the living cells in the form of the integer variables, NEIGHBORROW and NEIGHBORCOL. Begin Procedure GENERATE If (NLIMIT = 0) then For each of rhe eight sets of coordinates Call STRETCH End do
of the neighboring cells defined by LIFER0
W and LIFECOL
do
CLIFTON FINNEY
324
Else For each of the eight sets of coordinates of the neighboring cells defined by LIFEROW Set INDEX to I in preparation for the search of NEIGHBORINGARRAY to determine tf this neighbor is already present Whife (INDEX < = NLIMIT) and ~~NEIGHBORRO W e > NEIGH3O~NGARRA YfINDEX,i]f and (NEIGHBORCOL <:> N~IGHBORINGARRA Y[INDEX,2])) do Set INDEX to INDEX plus one End do If (INDEX < = NLIMIT) then Set NEIGHBORINGARRAY[I,4] TO NEIGHBORINGARRAY[I,4] plus one Else Cal! STRETCH End if End do End if End
and LIFECOL do
Begin Procedure STRETCH Set NLIMIT to NLIMIT plus one Set NEIGHBORINGARRAY[NLIMIT,1] to NEIGHBORRO W Set NEIGHBORINGARRAY[NLIMIT,Z] lo NEIGHBORCOL Call SEARCHLIFEARRAY returning DEADORALIVE Set NEIGHBORINGARRAY[NLIMiT,3] to DEADORALIVE Set NEIGHBORINGARRAY~NLI~IT,4] to 1 End
APPENDIX
B
Given the ROW and COLUMN fields of the living cell pointed to by NOWALIVE, this procedure requires the following enumerated type and variable definitions as a means of setting the R and C coordinates of the neighbors with the case statement. TYPE ALLDIRECTIONS
= ~NORTHW~ST, NORTH, NORTHEAST, WEST, EAST, SOUTHWEST, SOUTH, SOUTHEAST);
VAR DIRECTION: ALLDIRECTIONS; Begin Procedure ACTION For each DIRECTION from NORTHWEST to SOUTHEAST do Using a Case statement with DIRECTION as selector Set R and C to the row and column of a neighbor of the fit&g ceil pointed to by NO WALIVE End case While ~~I~STPOINTER .NEXT < > Nil) and (((R = FIRSTPOINTER .ROW) and (C > FIRSTPOINTER .COL)) or (R > FIRSTPOINTER .RO W)) do Set SECONDPOINTER to FIRSTPOINTER Set FIRSTPOINTER to the pointer field of FIRSTPOINTER searching the list for the neighbor with coordinates R and C End do If ((R = FIRSTPOINTER .RO W) and (C = FIRSTPOINTER .~OLUMN~~ then the neighbor is found Set FIRSTPOINTER .COUNT to FIRSTPOINTER .COUNT plus one Eke If ((R = FIRSTPOINTER .RO W) and (C < FIRSTPOINTER .COLCJMN)) or (R -KFIRSTPOINTER .RO W) then the neighbor is absent, but is to be inserted between FIRSTPOINTER and SECONDPOINTER Call NEW passing THIRDPOINTER Set the pointer field of THIR~POINTER to the pointer fieid of SECONDPOINTER Set the pointer field of SECONDPOINTER to THIRDPOINTER Set SECONDPOINTER to THIRDPOINTER Call SETFIELDS
Counting in the game of life Else the neighbor is absent and is to inserted at the end of the list Call NEW passing THIRDPOINTER Set the pointer jield of THIRDPOINTER to nil Set the pointer field of FIRSTPOINTER to THIRDPOINTER Call SETFIELDS End if End if I/ (DIRECTION = NORTH WEST) then Set NEWSECONDPOINTER TO SECONDPOINTER as a marker in the list where the current search began End i/ End do Set SECONDPOINTER to NEWSECONDPOINTER Set FIRSTPOINTER to the pointer field of SECONDPOINTER in preparation for the next search End Begin Procedure SETFIELDS With THIRDPOINTER A do Set ROW to R Set COLUMN to C Set STATE to DEAD Set COUNT to one End with End
325