86. Life

Like the Langton's Ant example solution, this one is fairly long but relatively straightforward, so I won't include a lot of code here. The most important piece of code is the following Tick event handler, which updates the world to create the next generation:

// The size of the world.
private int GridWid, GridHgt;

// The world.
private bool[,] World = null;

// True if we should wrap squares across the world's edges.
private bool WrapEdges = true;

// Move objects.
private void moveTimer_Tick(object sender, EventArgs e)
{
// See how many neighbors each squares has.
int[,] numNeighbors = new int[GridWid, GridHgt];
for (int y = 0; y < GridHgt; y++)
{
for (int x = 0; x < GridWid; x++)
{
// See if this square is occupied.
if (World[x, y])
{
// Add this square to its neighbors' counts.
for (int dx = -1; dx <= 1; dx++)
for (int dy = -1; dy <= 1; dy++)
if ((dx != 0) || (dy != 0))
{
if (WrapEdges)
{
// Wrap style.
int nx = (x + dx + GridWid) % GridWid;
int ny = (y + dy + GridHgt) % GridHgt;
numNeighbors[nx, ny]++;
}
else
{
// Discard style.
int nx = x + dx;
int ny = y + dy;
if ((nx >= 0) && (nx < GridWid) &&
(ny >= 0) && (ny < GridHgt))
numNeighbors[nx, ny]++;
}
}
}
}
}

// Repopulate the world.
bool changed = false;
for (int y = 0; y < GridHgt; y++)
{
for (int x = 0; x < GridWid; x++)
{
// See if the square is currently populated.
bool value = World[x, y];
if (value)
{
// Currently populated. See if it survives.
if ((numNeighbors[x, y] < 2) ||
(numNeighbors[x, y] > 3))
value = false;
}
else
{
// Currently unpopulated. See if we should populate it.
if (numNeighbors[x, y] == 3)
value = true;
}
if (World[x, y] != value) changed = true;
World[x, y] = value;
}
}

// If nothing changed, stop.
if (!changed) startToolStripMenuItem_Click(null, null);

// Redraw.
worldPictureBox.Refresh();
TurnNumber++;
turnLabel.Text = TurnNumber.ToString();
}

The form-level variables, GridWid and GridHgt, hold the size of the grid. The World array indicates which cells are live, with a true value meaning a live cell and a false value meaning a dead cell.

The WrapEdges value indicates whether the program should wrap the world around the grid's edges so cells along one edge are neighbors of cells on the opposite edge.

The moveTimer_Tick event handler creates an array to hold the number of neighbors that each grid cell has. It then loops over all of the cells. For each cell that is alive, the code loops through that cell's neighbors and increments their neighbor counts.

This is the step that wraps around the edges of the grid if desired. If the WrapEdges variable is true, then the code uses the modulus operator (%) to wrap the coordinates around the grid's edges vertically and horizontally.

Notice how the calculation adds the bounds before applying the % operator. That protects the program against negative indices. For example, suppose the width of the grid, GridWid, is 100, the code is considering cell [0, 0], and dx is -1. Then (x + dx) % GridWid would be (0 – 1) % 100 = -1 % GridWid = -1, which lies outside of the grid’s valid indices. Adding GridWid to the calculation gives (0 – 1 + 100) % 100 = 99 % 100 = 99, which is a valid index.

The calculation of the neighbor's Y coordinate is similar.

After it has calculated each cell's neighbor count, the code rebuilds the world. It loops over each cell and uses the cell's current state and neighbor count to determine whether the cell should now be alive.

There are two reasons why the program counts neighbors and updates the cell population in two steps. First, it is slightly faster than updating the cells in one pass. Because the code only adds to the neighbor counts for cells that are alive, it performs roughly 8 × A steps, where A is the number of live cells and 8 is the number of neighbors. In contrast, if the code examined every cell and counted its neighbors, it would need to examine 8 cells for every cell in the grid.

The second and more important reason for updating the world in two steps is to avoid using new cell values to update other cells. For example, suppose you update the cells one row at a time from top to bottom. After you have updated row 0, the new cell values in that row would affect the values in row 1, but we only want to consider the previous values in row 1 when calculating the new values in row 2. Storing neighbor counts in a separate array avoids that problem.

There's one more piece of code in this solution that I want to describe. The following LoadPattern method takes as an input an array of strings that uses zeros and ones to define a pattern of cells and uses those strings to initialize the program's cells:

// Load a pattern defined by strings of 0s and 1s.
private void LoadPattern(string[] pattern)
{
World = new bool[GridWid, GridHgt];
int x = (GridWid - pattern[0].Length) / 2;
int y = (GridHgt - pattern.Length) / 2;
for (int iy = 0; iy < pattern.Length; iy++)
for (int ix = 0; ix < pattern[iy].Length; ix++)
World[x + ix, y + iy] = (pattern[iy][ix] == '1'),

TurnNumber = 0;
turnLabel.Text = "0";
worldPictureBox.Refresh();
}

This method creates a new, blank World array. It then calculates starting X and Y positions where it should start building the pattern to center the pattern's cells in the world grid.

The code then loops through the strings in the pattern array. For each string, the code loops through the string's characters and sets the corresponding World entries to true where the pattern contains a 1.

The example solution uses the LoadPattern method to let the user load a number of predefined patterns. For example, the following code loads one of the most famous Life patterns, which is named Glider:

private void gliderToolStripMenuItem_Click(object sender, EventArgs e)
{
string[] pattern =
{
"010",
"001",
"111",
};
LoadPattern(pattern);
}

Download the Life example solution to see additional details.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset