Conway's Game of Life

What I Developed

I implemented John Conway's Game of Life in Java using threads. It's important to note that I've written much more complicated software than this in my life but I've decided to include this project in my resume as opposed to others due to the "games" impact on cyberculture. Below are the steps I took in order to implement my very own Game of Life.

1. Decide the Parameters of my Implementation

Which language to use (Java), the size of the board (20x20), and how the logic would be implemented (using threads) was predefined since I was working on this project for scholastic credit. I was didn't implent robust expection handling for this assignment, since I was instructed not to, though I normally would have. Essentially, I was tasked with implementing the Game of Life within the aformentioned, predefined parameters.

With that being said, I had the freedom to decide how I would organize the code body. I opted to employ an object-oriented approach in order to modularize the code as much as possible to allow for easier troubleshooting in the future.

2. Define & Implement the Rules

My first order of business when actually implementing the game was to implement the rule set using threads. If you're not familiar with the game, Conway's Game of Life was originally played on a flat grid made up of square cells. These cells can be either live (with a token in it) or dead (no tokens in it). In my version, live cells are represented by an 'X' and dead cells by an 'O'. In theory, the size of the grid is infinite, but small boards will do for initial play. This is a solitary game, or one with just one player, and the play of a typical game goes like this:

  • Player chooses an initial set up.
  • Rules are applied to see what happens in the next generation.
  • Play continues until one of three things happen - all cells are dead, no cells change from one generation to the next, or the pattern flips back and forth between two or more positions. (In my adaptation of the game, playtime is decided by the user via the number of generations they choose.)

At the heart of this game are four simple rules that determine if a cell is alive or dead.

  • 1.
    Births: Each dead cell adjacent to exactly three live neighbors will become live in the next generation.
  • 2.
    Death by isolation: Each live cell with one or fewer live neighbors will die in the next generation.
  • 3.
    Death by overcrowding: Each live cell with four or more live neighbors will die in the next generation.
  • 4.
    Survival: Each live cell with either two or three live neighbors will remain alive for the next generation.

I coded the above rule set in its own class that implements Java's Runnable interface as follows:


public boolean isAlive()
{
    int liveNeighbor = 0;
    
    for (int i = row - 1; i <= row + 1; i++)
    {
        for (int j = col - 1; j <= col + 1; j++)
        {
            if ((i != row || j != col) && i >= 0 && j >= 0 && i < 20 && j < 20 && grid[i][j] == 'X')
            {
                liveNeighbor++;
            }
        }
    }
    if (grid[row][col] == 'O' && liveNeighbor == 3)
    {
        return true;
    }      
    if (grid[row][col] == 'X' && (liveNeighbor == 2 || liveNeighbor == 3))
    {
        return true;
    }
        return false;
}

@Override
public void run()
{
    boolean living = isAlive();
    if (living)
    {
        nextGrid[row][col] = 'X';
    }
    else
    {
        nextGrid[row][col] = 'O';
    }
}
                        

3. Implement Iterative Logic using Threads

I implemented the iterative logic of the program using threads in the driver class as follows:


public void useThreads()
{
    Thread threads[] = new Thread[400];
    int index = 0;
    for (int row = 0; row < 20; row++)
    {
        for (int col = 0; col < 20; col++)
        {
            threads[index] = new Thread(new Threads(row, col, grid, nextGrid));
            threads[index].start();
            index++;
        }
    }
    try
    {
        for (int cells = 0; cells < 400; cells++)
        {
            threads[cells].join();
        }         
    }
    catch (Exception e)
    {
        System.out.println(e);
    }
    for (int row = 0; row < 20; row++)
    {
        for (int col = 0; col < 20; col++)
        {
            grid[row][col] = nextGrid[row][col];
        }
    }  
}
                        

The above code segment allows each cell in the game to be analyzed & set to either alive or dead concurrently. If I was to implement this game without threads the execution time of the program would be dramatically longer.

4. Implement the Main Function

I implemented the main function as follows:


public static void main(String[] args)
{
    Driver game1 = new Driver();  
    int generations = game1.getGraph();
    
    for (int i = 0; i < generations; i++)
    {
        game1.useThreads();
    }
    
    System.out.println("The final configuration (" + generations + "):");
    
    for (int row = 0; row < 20; row++)
    {
        for (int col = 0; col < 20; col++)
        {
            System.out.print(String.valueOf(game1.grid[row][col]));
        }
        System.out.println("");
    }
}
                        

Conclusion

This game is a beautiful exemplification of how simple, granular rules on a micro-level can create incredible complexities on a macro-level. One of the most interesting features of this game (and subject as a whole) is that the rules must be tuned perfectly to produce such rich systems of complexity. That is to say that, an arbitraty rule or arbitrary set of rules, more often than not, wouldn't generate any kind of interesting behavior. This is actually a topic of discussion at this very moment at the cutting edge of mathematic research. For instance, Stephen Wolfram's Cellular Automata is somewhat of an Ode to and exploration of this game and the fundamental mystery of organized, foundational structures influencing the evolutionary behavior of grander systems.

Full Program