HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

Simple Conway's Game of Life implementation in Java

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
simpleconwayjavagamelifeimplementation

Problem

I wrote a simple implementation of Conway's Game of Life in Java using 2 arrays and for loop and used StdDraw library for plotting generations.
It turned out that algorithm works ok for little number of cells (e.g. glider pattern), but becomes terribly slow for big number of cells (e.g. random filling) even with small array sizes (e.g., 100*100 cells). What can be the bottlenecks of my algorithm and how can I improve it?

```
import java.util.*;
import edu.princeton.cs.introcs.StdDraw;

public class GameOfLife {
public static void main(String[] args) {

// the size of cells' array
final int ROWS_NUM = 500;
final int COLS_NUM = 500;

Boolean[][] curGen = new Boolean[ROWS_NUM][COLS_NUM];

// sets dead cells array
for (int row = 0; row 3)) {
nextGen[row][col] = false;
}

// cell lives on to next generation
if (numOfNeighbours == 2) {
nextGen[row][col] = curGen[row][col];
}

// cell either stays alive, or is born
if (numOfNeighbours == 3) {
nextGen[row][col] = true;
}
}
}
return nextGen;
}

// counts cell's neighbours
public static int countCellNeighbours(Boolean[][] curGen, int rowsNum, int colsNum, int row, int col) {
int numOfNeighbours = 0;

// decides which neighbour cells to count (for edge cells
// checks for neighbours from opposite edge)
// not edge cells
if ((row > 0) && (row 0) && (col < colsNum - 1)) {
if (curGen[row - 1][col - 1]) {
numOfNeighbours++;
}
if (curGen[row - 1][col]) {
numOfNeighbours++;
}
if (curGen[row - 1][col + 1]) {
numOfNeighbours++;
}
if (curGen[row][col - 1]) {
numOfNeighbours++;
}
if (curGen[row][col + 1]) {
numOfNeighbours++;
}
if (curGen[row + 1][col - 1]) {
numOfNeighbours++;

Solution

Performance

-
StdDraw.show(int)
If you take a look at the documentation for StdDraw.show(int), you'll notice the following: It also speeds up drawing a huge number of shapes (call {@code show(0)} to defer drawing on screen, draw the shapes, and call {@code show(0)} to display them all on screen at once). With that in mind, let's take a look at your drawing loop:

// infinitely draws field
while (true) {
    curGen = countNextGen(curGen, ROWS_NUM, COLS_NUM);
    StdDraw.clear();
    for (int row = 0; row < ROWS_NUM; row++) {
        for (int col = 0; col < COLS_NUM; col++) {
            if (curGen[row][col] == true) {
                StdDraw.point(col, row);
            }
        }
    }
}


Using the suggestion in the documentation, we add StdDraw.show(0) calls before and after painting each generation. We end up with the following:

// infinitely draws field
while (true) {
    curGen = countNextGen(curGen, ROWS_NUM, COLS_NUM);
    StdDraw.show(0);
    StdDraw.clear();
    for (int row = 0; row < ROWS_NUM; row++) {
        for (int col = 0; col < COLS_NUM; col++) {
            if (curGen[row][col] == true) {
                StdDraw.point(col, row);
            }
        }
    }
    StdDraw.show(0);
}


If we run the application now, we'll already see a huge visual improvement! Moral of the story: get familiar with the libraries you're using by reading the documentation. Any good library will provide code samples of common use cases.

How (and why) this works:
StdDraw (and just about any other graphics library) has the ability to do what's called Double Buffering. In short, you put a bunch of stuff a bunch of times in the off-screen buffer, and when you're ready it will all be drawn at once to the on-screen buffer. The alternative is to constantly be drawing things one at a time to the on-screen buffer, which is what your code does currently. In fact, it's so slow that you can see each generation being drawn pixel by pixel, line by line. By using StdDraw.show(0), you're saying: "Wait! Let me tell you everything I want to see before you go make it happen." instead of saying: "Here, draw this" (StdDraw draws it) "Oh, draw this too" (StdDraw goes and draws that too) "And this too", back and forth, back and forth, etc. etc.

-
Boolean vs. boolean
One of the first things that jumped out at me was the use of the Boolean object as opposed to the boolean primitive. Here's a good post detailing the differences between Boolean and boolean. In your situation, there isn't a need to be creating Boolean objects - the primitive will be sufficient. This is a simple change in your code; just do a find-replace for all instances of "Boolean" and replace them with "boolean"!

Design

-
Separating the view
This review is tagged beginner, but it's never too early to learn good design practices! Enter SoC. SoC stands for Separation of Concerns, and essentialy means that a piece of code should have one and only one responsibility. This will make your code more modular, and therefore reusable. In a simple case such as this it may not seem important, but it's simple to implement and it's better to learn on simple examples before jumping to a complex application!

In this code, the biggest change is to pull out the "view" code from the rest. That is, anything dealing with drawing on the screen should be in its own view class. The idea is to separate the UI from business logic (in this case the business logic will be determining which cells are to be drawn, etc.). The view should be dumb and have little to no logic. A common design pattern to follow is called MVP (Model-View-Presenter). I won't go into the details of MVP here because your application is not driven by user interaction. That is, you aren't responding to button clicks, etc. Once you kick off the simulation, you kick back and enjoy a cold one.

In practice, we can start with the following template:

public class GameOfLifeView {

    public GameOfLifeView() {

    }

}

public class GameOfLifePresenter {

    private final GameOfLifeView view;

    public GameOfLifePresenter(final GameOfLifeView view) {
        this.view = view;
    }

}

public class GameOfLifeApp {

    public GameOfLifeApp() {
        new GameOfLifePresenter(new GameOfLifeView());
    }

    public static void main(String... args) {
        new GameOfLifeApp();
    }

}


By passing in the view to the presenter, we allow the presenter to handle the business logic and control what happens to the view.

But, where's the "M" from MVP?? I'm glad you asked! The model is what maintains state in an program. In our case, it is the 2D boolean array. We could make a Generation class which just has a boolean[][] in order to better illustrate MVP, but it's not really necessary.

Adding in our "model":

```
public class GameOfLifeView {

public GameOfLifeView() {

}

/**
* Returns the non-negative, non-zero height of drawable area.
*/
public int getHeight()

Code Snippets

// infinitely draws field
while (true) {
    curGen = countNextGen(curGen, ROWS_NUM, COLS_NUM);
    StdDraw.clear();
    for (int row = 0; row < ROWS_NUM; row++) {
        for (int col = 0; col < COLS_NUM; col++) {
            if (curGen[row][col] == true) {
                StdDraw.point(col, row);
            }
        }
    }
}
// infinitely draws field
while (true) {
    curGen = countNextGen(curGen, ROWS_NUM, COLS_NUM);
    StdDraw.show(0);
    StdDraw.clear();
    for (int row = 0; row < ROWS_NUM; row++) {
        for (int col = 0; col < COLS_NUM; col++) {
            if (curGen[row][col] == true) {
                StdDraw.point(col, row);
            }
        }
    }
    StdDraw.show(0);
}
public class GameOfLifeView {

    public GameOfLifeView() {

    }

}

public class GameOfLifePresenter {

    private final GameOfLifeView view;

    public GameOfLifePresenter(final GameOfLifeView view) {
        this.view = view;
    }

}

public class GameOfLifeApp {

    public GameOfLifeApp() {
        new GameOfLifePresenter(new GameOfLifeView());
    }

    public static void main(String... args) {
        new GameOfLifeApp();
    }

}
public class GameOfLifeView {

    public GameOfLifeView() {

    }

    /**
     * Returns the non-negative, non-zero height of drawable area.
     */
    public int getHeight() {

    }

    /**
     * Returns the non-negative, non-zero width of drawable area.
     */
    public int getWidth() {

    }

}

public class GameOfLifePresenter {

    private final GameOfLifeView view;
    private final boolean[][] currentGeneration;

    public GameOfLifePresenter(final GameOfLifeView view) {
        this.view = view;
        this.currentGeneration = new boolean[view.getHeight()][view.getWidth()];
    }

}
public class GameOfLifeView {

    public GameOfLifeView() {
        StdDraw.setCanvasSize(width, height);
        StdDraw.setYscale(0, height);
        StdDraw.setXscale(0, width);
        StdDraw.setPenRadius(0);
        StdDraw.setPenColor(StdDraw.BLACK);
    }

    public int getHeight() {...}
    public int getWidth() {...}

    /**
     * Draws the given generation to the screen.
     */
    public void drawGeneration(final boolean[][] generation) {
        StdDraw.show(0);
        StdDraw.clear();
        for (int row = 0; row < generation.length; row++) {
            for (int col = 0; col < generation[row].length; col++) {
                if (generation[row][col] == true) {
                    StdDraw.point(col, row);
                }
            }
        }
        StdDraw.show(0);
    }

}

Context

StackExchange Code Review Q#126056, answer score: 8

Revisions (0)

No revisions yet.