Java Arena

The Game of Life

The Game of Life was invented by the mathematician John Horton Conway around 1970. It isn't a game you play: you set up a pattern and watch what happens. The board is a grid of cells, and each cell is either alive or dead. Every cell has eight neighbors: the cells above, below, to the left, to the right and on the four diagonals. From one generation to the next, every cell follows the same rules at the same time:

  • A live cell with 2 or 3 live neighbors stays alive.
  • A live cell with fewer than 2 live neighbors dies, as if lonely, and one with more than 3 dies, as if overcrowded.
  • A dead cell with exactly 3 live neighbors comes alive.
  • Any other dead cell stays dead.

That's all, and yet some patterns blink, some stay still, some die out, and some travel across the board. In this project you write a program that reads a pattern from a file and prints a few generations of it as text, with O for a live cell and . for a dead one.

When it's finished, the program reads three lines: the name of a pattern file, how many generations to show, and wrap or edges (whether the board wraps around at its edges). With this glider.txt, where the lines that start with ! are comments,

!Name: Glider
!It moves one cell down and one to the right every four generations.
.O....
..O...
OOO...
......
......

and the input glider.txt, 4 and wrap, it prints:

Generation 0: 5 alive
.O....
..O...
OOO...
......
......

Generation 1: 5 alive
......
O.O...
.OO...
.O....
......

Generation 2: 5 alive
......
..O...
O.O...
.OO...
......

Generation 3: 5 alive
......
.O....
..OO..
.OO...
......

Generation 4: 5 alive
......
..O...
...O..
.OOO..
......

After four generations the glider has its first shape again, one row lower and one column further right.

What you need

Everything up to the end of part 12. Above all two-dimensional arrays, nested loops and the cells around a cell (module 47), and also classes with a constructor and toString (modules 17 and 18), StringBuilder (module 39), and reading a file with Files.readAllLines and throws IOException (module 42).

How you build it

The program has two classes: Board, which keeps the cells in a boolean[][] and knows the rules, and Main, which reads the pattern and prints the generations. Most of the work is in Board, and the checks of the first milestones call its methods directly.

  1. A grid of cells: create the board, bring cells to life, and show it as text.
  2. Counting neighbors: how many live cells are around a cell, and whether it lives on.
  3. One generation: the whole board takes one step, every cell at the same time.
  4. Wrapping around: the edges of the board meet, so a glider leaves on one side and comes back on the other.
  5. Patterns from a file: read a board from a text file of O and ..
  6. Generations as text: main puts it all together, and stops early when nothing changes.

Milestones

  1. A grid of cells
  2. Counting neighbors
  3. One generation
  4. Wrapping around
  5. Patterns from a file
  6. Generations as text

Milestone 1: A grid of cells

Board.java has the field for the cells: cells is a boolean[][], where true means the cell is alive. Main already uses some of these methods, so the program compiles once you've written them all. Write in Board:

  1. A constructor Board(int rows, int columns) that creates a grid of that many rows and columns, with every cell dead.
  2. getRows() and getColumns(), which return the number of rows and of columns. The boards always have at least one row and one column.
  3. setAlive(int row, int column), which makes that cell alive, and isAlive(int row, int column), which returns whether it's alive. Rows and columns are counted from 0, as array indexes are.
  4. countAlive(), which returns how many cells of the board are alive.
  5. toString(), which returns the board one row per line: O (the capital letter) for a live cell and . for a dead one. Put a line break (\n) between the rows, but not after the last one, so System.out.println(board) prints no empty line at the end.

The check runs:

Board board = new Board(3, 5);
board.setAlive(0, 1);
board.setAlive(1, 2);
board.setAlive(2, 4);
System.out.println(board.getRows() + " by " + board.getColumns());
System.out.println(board);
System.out.println(board.countAlive());
System.out.println(board.isAlive(1, 2));
System.out.println(board.isAlive(2, 3));

and expects:

3 by 5
.O...
..O..
....O
3
true
false

Milestone 2: Counting neighbors

Whether a cell lives on depends on its eight neighbors. Add two methods to Board:

  1. countNeighbors(int row, int column) returns how many of the eight cells around that cell are alive. The cell itself doesn't count. A cell on the edge of the board has fewer neighbors: a place outside the board counts as a dead cell.
  2. willLive(int row, int column) returns whether the cell is alive in the next generation, by the rules of the game:
    • a live cell stays alive with 2 or 3 live neighbors, and dies otherwise;
    • a dead cell comes alive with exactly 3 live neighbors, and stays dead otherwise.

The check runs:

Board board = new Board(4, 5);
board.setAlive(1, 1);
board.setAlive(1, 2);
board.setAlive(1, 3);
board.setAlive(0, 4);
System.out.println(board);
System.out.println(board.countNeighbors(1, 2));
System.out.println(board.countNeighbors(0, 2));
System.out.println(board.countNeighbors(0, 0));
System.out.println(board.countNeighbors(3, 4));
System.out.println(board.willLive(1, 2));
System.out.println(board.willLive(1, 1));
System.out.println(board.willLive(0, 2));
System.out.println(board.willLive(2, 2));

and expects:

....O
.OOO.
.....
.....
2
3
1
0
true
false
true
true

Milestone 3: One generation

Now the whole board takes a step. Every cell changes at the same time: the next generation is worked out from the current one alone. So the program can't change the cells one by one in cells, because a cell it has already changed would give its neighbors the wrong count. Build the next generation in a new array instead, and put it in place when it's complete.

  1. Add a field generation, an int that is 0 for a new board.
  2. Write step(), which creates a new boolean[][] of the same size, fills it with willLive for every cell, and then makes it the board's cells. It also adds one to generation.
  3. Write getGeneration(), which returns how many steps the board has taken.

To watch it in your own program, add board.step(); and another System.out.println(board); at the end of main. The check runs:

Board board = new Board(5, 5);
board.setAlive(2, 1);
board.setAlive(2, 2);
board.setAlive(2, 3);
board.step();
System.out.println(board);
System.out.println(board.getGeneration());
board.step();
System.out.println(board);
System.out.println(board.getGeneration());

and expects a blinker, a row of three that turns into a column and back:

.....
..O..
..O..
..O..
.....
1
.....
.....
.OOO.
.....
.....
2

Milestone 4: Wrapping around

A glider on the board you have now soon reaches an edge, turns into a block and stops. Many Life programs let the edges wrap around instead: the row above the top row is the bottom row, and the column to the left of the first column is the last column. The board becomes like the surface of a doughnut, with no edges at all, and a glider that leaves on one side comes back on the other.

  1. Add a field wrapping, a boolean that is false for a new board, and a method setWrapping(boolean wrapping) that sets it.
  2. When wrapping is true, countNeighbors wraps around: on a board of 5 rows, row -1 is row 4 and row 5 is row 0, and the same for the columns. When it's false, the method counts as before.

willLive and step use countNeighbors, so they wrap too, with no change. The check runs:

Board board = new Board(5, 5);
board.setAlive(1, 0);
board.setAlive(2, 0);
board.setAlive(3, 0);
board.setWrapping(true);
System.out.println(board.countNeighbors(2, 4));
System.out.println(board.countNeighbors(0, 0));
board.step();
System.out.println(board);

and expects a blinker on the left edge to turn across it:

3
1
.....
.....
OO..O
.....
.....

Milestone 5: Patterns from a file

Setting cells one by one is slow. Life patterns are often kept in text files that look like a printed board, and many Life programs read files like this one:

!Name: Glider
!It moves one cell down and one to the right every four generations.
.O
..O
OOO
......

A line that starts with ! is a comment. Every other line is a row of the board, from the top: O is a live cell, and any other character a dead one. The rows can have different lengths. The board is as wide as the longest row, and a shorter row continues with dead cells. An empty line is a row of dead cells.

Write a method public static Board readPattern(String fileName) throws IOException in Main that reads the file and returns its board. The files in the checks have at least one row that isn't empty. When the file can't be read, the method doesn't catch the exception: it leaves it to the caller, which is what throws IOException says. The check gives the program the file above as glider.txt and runs:

Board board = readPattern("glider.txt");
System.out.println(board.getRows() + " by " + board.getColumns());
System.out.println(board);
System.out.println(board.countAlive());

and expects:

4 by 6
.O....
..O...
OOO...
......
5

Milestone 6: Generations as text

Time to put it all together in main. Replace the example board in main with a program that reads three lines of input:

  1. the name of a pattern file,
  2. how many generations to show after the first one,
  3. wrap for a board that wraps around at its edges, or edges for one that doesn't.

The program reads the board with readPattern and prints generation 0, then takes a step and prints the next generation, until it has shown the number of generations that was asked for. Each generation starts with a line such as Generation 2: 5 alive (its number and how many cells are alive), followed by the board. An empty line comes before every generation except the first.

Two more things:

  • Some patterns stop changing, such as a block, or a board where everything has died. Change step() in Board to return a boolean: true when at least one cell changed, and false when the new generation is exactly the old one. When a step changes nothing, the program prints the empty line and Nothing changes after generation 3. (with the number of the last generation it printed), and stops.
  • When the file can't be read, the program prints Could not read nothing.txt. (with the name that was given) and nothing else.

With this blinker.txt

!Blinker
.....
.OOO.
.....

and the input blinker.txt, 2 and edges, the program prints:

Generation 0: 3 alive
.....
.OOO.
.....

Generation 1: 3 alive
..O..
..O..
..O..

Generation 2: 3 alive
.....
.OOO.
.....