3
\$\begingroup\$

I've been working on a basic implementation of Conway's Game of Life over the last day, and I've just finished a 'playable' version.

If anyone has the time, would you mind making any suggestions on code improvements that I can make?

Thanks!

GameOfLife.java

package robbie.gameoflife.main;

import java.awt.Dimension;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;

import robbie.gameoflife.cells.Cell;
import robbie.gameoflife.cells.CellManager;

public class GameOfLife {

    private static final String TITLE = "Game of Life";

    public static final int CELL_SIZE = 10, CELL_AMOUNT = 50;
    public static final int WIDTH = CELL_SIZE * CELL_AMOUNT, HEIGHT = WIDTH;

    public static final Dimension DIMENSIONS = new Dimension(WIDTH, HEIGHT);

    private static final boolean RUNNING = true;

    public static final Cell[][] CELLS = new Cell[CELL_AMOUNT][CELL_AMOUNT];

    public static void main(String[] args) {
        JFrame frame = new JFrame(TITLE);
        frame.setSize(DIMENSIONS);
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);

        GameCanvas canvas = new GameCanvas();
        frame.add(canvas);
        canvas.createBufferStrategy(3);

        loadCells();

        CELLS[10][10].setAlive(true);
        CELLS[10][11].setAlive(true);
        CELLS[10][12].setAlive(true);
        CELLS[11][10].setAlive(true);
        CELLS[12][10].setAlive(true);
        CELLS[12][11].setAlive(true);
        CELLS[12][12].setAlive(true);
        CELLS[11][12].setAlive(true);

        startGameLoop(canvas);
    }

    private static void loadCells() {
        for (int x = 0; x < CELLS.length; x++) {
            for (int y = 0; y < CELLS[x].length; y++) {
                if (CELLS[x][y] == null)
                    CELLS[x][y] = new Cell(x, y);
            }
        }
    }

    private static void startGameLoop(GameCanvas canvas) {
        Timer timer = new Timer();

        if (RUNNING) {
            timer.schedule(new TimerTask() {

                @Override
                public void run() {
                    CellManager.updateCells();
                    canvas.repaint();
                }

            }, 1000, 1000);
        }
    }

}

GameCanvas.java

package robbie.gameoflife.main;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.util.Random;

import robbie.gameoflife.cells.Cell;

public class GameCanvas extends Canvas {

    public GameCanvas() {
        super();

        this.setSize(GameOfLife.WIDTH, GameOfLife.HEIGHT);
        this.setBackground(Color.WHITE);
        this.setFocusable(true);
        this.setVisible(true);
    }

    public void paint(Graphics g) {
        Graphics graphics = this.getGraphics();
        graphics.clearRect(0, 0, GameOfLife.WIDTH, GameOfLife.HEIGHT);
        Random random = new Random();
        graphics.setColor(new Color(random.nextFloat(), random.nextFloat(), random.nextFloat()));

        for (Cell[] cells : GameOfLife.CELLS) {
            for (Cell cell : cells) {
                if (cell.isAlive()) {
                    int size = GameOfLife.CELL_SIZE;
                    int x = cell.getX() * size;
                    int y = cell.getY() * size;

                    graphics.fillRect(x, y, size, size);
                }
            }
        }
    }

}

Cell.java

package robbie.gameoflife.cells;

public class Cell {

    private int x, y;
    private boolean alive = false;

    public Cell(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public boolean isAlive() {
        return alive;
    }

    public void setAlive(boolean alive) {
        this.alive = alive;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void toggleAlive() {
        this.alive = !this.alive;
    }

}

CellManager.java

package robbie.gameoflife.cells;

import java.util.ArrayList;

import robbie.gameoflife.main.GameOfLife;

public class CellManager {

    public static ArrayList<Cell> getNeighbourCells(Cell cell) {
        ArrayList<Cell> neighbourCells = new ArrayList<Cell>();
        int cellX = cell.getX();
        int cellY = cell.getY();

        for (int x = cellX - 1; x <= cellX + 1; x++) {
            for (int y = cellY - 1; y <= cellY + 1; y++) {
                if (x < 0 || x >= GameOfLife.CELL_AMOUNT || y < 0 || y >= GameOfLife.CELL_AMOUNT)
                    continue;

                Cell neighbourCell = GameOfLife.CELLS[x][y];
                if (neighbourCell.equals(cell))
                    continue;

                if (neighbourCell.isAlive())
                    neighbourCells.add(neighbourCell);
            }
        }

        return neighbourCells;
    }

    public static int getNeighbourCount(Cell cell) {
        return getNeighbourCells(cell).size();
    }

    public static void updateCells() {
        ArrayList<Cell> deadCells = new ArrayList<Cell>();
        ArrayList<Cell> aliveCells = new ArrayList<Cell>();

        for (Cell[] cells : GameOfLife.CELLS) {
            for (Cell cell : cells) {
                int neighbourCount = CellManager.getNeighbourCount(cell);
                if (neighbourCount < 2 || neighbourCount > 3)
                    deadCells.add(cell);
                else if (cell.isAlive() && (neighbourCount == 2 || neighbourCount == 3))
                    aliveCells.add(cell);
                else if (!cell.isAlive() && neighbourCount == 3)
                    aliveCells.add(cell);
            }
        }

        for (Cell cell : aliveCells)
            cell.setAlive(true);

        for (Cell cell : deadCells)
            cell.setAlive(false);
    }

}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Prefer interfaces to implementations

    public static ArrayList<Cell> getNeighbourCells(Cell cell) {
        ArrayList<Cell> neighbourCells = new ArrayList<Cell>();

and

        ArrayList<Cell> deadCells = new ArrayList<Cell>();
        ArrayList<Cell> aliveCells = new ArrayList<Cell>();

You never use ArrayList specific functionality though, just List functionality (add and implicitly the iterator).

If you make these

    public static List<Cell> getNeighbourCells(Cell cell) {
        List<Cell> neighbourCells = new ArrayList<>();

and

        List<Cell> deadCells = new ArrayList<>();
        List<Cell> aliveCells = new ArrayList<>();

Then you can change implementations by simply changing the right side.

Note that the <> syntax only works in newer versions of Java.

Prefer doing it right to checking if right

        int cellX = cell.getX();
        int cellY = cell.getY();
        for (int x = cellX - 1; x <= cellX + 1; x++) {
            for (int y = cellY - 1; y <= cellY + 1; y++) {
                if (x < 0 || x >= GameOfLife.CELL_AMOUNT || y < 0 || y >= GameOfLife.CELL_AMOUNT)
                    continue;

You can simplify this with Math.min and Math.max.

        int startX = cell.getX() - 1;
        int startY = cell.getY() - 1;

        int maxX = Math.min(startX + 2, GameOfLife.CELL_AMOUNT);
        int maxY = Math.min(startY + 2, GameOfLife.CELL_AMOUNT);

        for (int x = Math.max(startX, 0); x <= maxX; x++) {
            for (int y = Math.max(startY, 0); y <= maxY; y++) {

Now we don't check nine times (including once with the original values) if the x, y pair is in bounds (four checks per pair). We only check each boundary once (four boundaries).

The continue is unnecessary here

                Cell neighbourCell = GameOfLife.CELLS[x][y];
                if (neighbourCell.equals(cell))
                    continue;
                if (neighbourCell.isAlive())
                    neighbourCells.add(neighbourCell);

While the early bailout is often helpful, I'm not sure that I'd do it here. Consider

                Cell neighbourCell = GameOfLife.CELLS[x][y];
                if (!neighbourCell.equals(cell) && neighbourCell.isAlive()) {
                    neighbourCells.add(neighbourCell);
                }

Since that's now the entire body of the loop, we don't need to continue.

I also added { and } because I prefer to always use the block form. There's a set of problems that can arise with inattentive editing of the statement form that don't bother the block form. I just find it easier to always use it. The maintenance cost is cheaper in my opinion.

Don't Repeat Yourself

                if (neighbourCount < 2 || neighbourCount > 3)
                    deadCells.add(cell);
                else if (cell.isAlive() && (neighbourCount == 2 || neighbourCount == 3))
                    aliveCells.add(cell);
                else if (!cell.isAlive() && neighbourCount == 3)
                    aliveCells.add(cell);

This is the same thing as

                if (neighbourCount < 2 || neighbourCount > 3) {
                    deadCells.add(cell);
                } else if (cell.isAlive()) {
                    aliveCells.add(cell);
                } else if (neighbourCount == 3) {
                    aliveCells.add(cell);
                }

The else statement lets you know that the expression in the if is not true. You don't have to check it again. So we know there are 2 or 3 neighbors in the first and we know that the cell is alive in the second (and we still know that there are 2 or 3 neighbors, although that doesn't help since 2 and 3 neighbor cells have different behavior when not alive).

WE can continue to simplify:

                if (neighbourCount < 2 || neighbourCount > 3) {
                    deadCells.add(cell);
                } else if (cell.isAlive() || neighbourCount == 3) {
                    aliveCells.add(cell);
                }

Given that what you are doing is setting the alive status, we can do even better in terms of efficiency.

                if (neighbourCount < 2 || neighbourCount > 3) {
                    deadCells.add(cell);
                } else if (!cell.isAlive() && neighbourCount == 3) {
                    aliveCells.add(cell);
                }

Now the second if only puts the cell in the list if it is not alive but should be next round. We can do the same thing to the first if.

                if (cell.isAlive() && (neighbourCount < 2 || neighbourCount > 3)) {
                    deadCells.add(cell);
                } else if (!cell.isAlive() && neighbourCount == 3) {
                    aliveCells.add(cell);
                }

But now we call cell.isAlive() twice, once in each branch. This makes the else redundant. We could just as well write

                if (cell.isAlive() && (neighbourCount < 2 || neighbourCount > 3)) {
                    deadCells.add(cell);
                }

                if (!cell.isAlive() && neighbourCount == 3) {
                    aliveCells.add(cell);
                }

or

                if (cell.isAlive() && neighbourCount != 2 && neighbourCount != 3) {
                    deadCells.add(cell);
                }

                if (!cell.isAlive() && neighbourCount == 3) {
                    aliveCells.add(cell);
                }

This takes the negation of the or expression so that we can do without the extra parentheses.

Or even

                if (cell.isAlive()) {
                    if (neighbourCount != 2 && neighbourCount != 3) {
                        deadCells.add(cell);
                    }
                } else if (neighbourCount == 3) {
                    aliveCells.add(cell);
                }

Although the complexity of nesting might make it less readable. I would consider any of these last three to be reasonable.

Note that even in your original, there was a case, !cell.isAlive() && neighbourCount == 2 that doesn't get set either way. This just makes that clearer.

Naming

Also consider changing deadCells and aliveCells to something that makes it clearer that the status is changing, e.g. dyingCells and birthingCells. Otherwise, you could just write

                if (neighbourCount == 3 || (neighbourCount = 2 && cell.isAlive())) {
                    aliveCells.add(cell);
                } else {
                    deadCells.add(cell);
                }

This sets every cell's status explicitly.

A bit wasteful though, as the majority of cells will not be alive and not be changing status.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.