Please read the general introduction to the game of life kata first!
We are going to build the following code, which is a showcase of code should express intent, Kent Beck's design rule number 2!
public static List<Cell> iterateGameboard(final List<Cell> gameboard) {
return gameboard
.stream()
.map(toDeadCell(which(isLiving, and(),
which(hasLessThanTwo(livingNeighboursIn(gameboard)), or(), hasMoreThanThree(livingNeighboursIn(gameboard))))))
.map(toLivingCell(which(isDead, and(), hasExactlyThree(livingNeighboursIn(gameboard)))))
.collect(Collectors.toList());
}
The approach here is an almost literal TDD version of the marvelous functional solution proposed in this excellent post and the associated code repository mentioned therein.
First, create an intial Java kata set-up as described here.
Next, go to the newly created project directory and consult
the provided README.md
in there.
Let's write our first specification(s) for predicates of living and dead cells.
Predicates for living and dead cells
import static gameoflife.Cell.*;
class GameOfLifeTest {
@Test
void isLivingPredicate() {
assertNotNull(Optional.of(newLivingCell()).filter(isLiving).get());
assertTrue(Optional.of(newDeadCell()).filter(isLiving).isEmpty());
}
@Test
void isDeadPredicate() {
assertNotNull(Optional.of(newDeadCell()).filter(isDead).get());
assertTrue(Optional.of(newLivingCell()).filter(isDead).isEmpty());
}
}
where the newDeadCell()
, newLivingCell()
, isAlive
, and isDead
methods and predicates are (to be) defined in the Cell
class.
Definition of the Cell
class that makes the test pass
public class Cell {
private final boolean alive;
private Cell(final boolean alive) {
this.alive = alive;
}
private boolean isAlive() {
return alive;
}
public static final Cell newLivingCell() {
return new Cell(true);
}
public static final Cell newDeadCell() {
return new Cell(false);
}
public static Predicate<Cell> isAlive = Cell::isLiving;
public static Predicate<Cell> isDead = isLiving.negate();
}
We may want to refactor the tests a bit to express intent more clearly
Refactoring the tests to express intent more clearly
class GameOfLifeTest {
private boolean isDead(final Cell cell) {
return !Optional.of(cell).filter(isDead).isEmpty();
}
private boolean isLiving(final Cell cell) {
return !Optional.of(cell).filter(isLiving).isEmpty();
}
@Test
void isLivingPredicate() {
assertTrue(isLiving(newLivingCell()));
assertFalse(isLiving(newDeadCell()));
}
@Test
void isDeadPredicate() {
assertFalse(isDead(newLivingCell()));
assertTrue(isDead(newDeadCell()));
}
}
We should be able to change cells from living to dead and vice versa. In functional programming, this means creating new cells, as we cannot change state (because of immutability!).
Note that we would like to conditionally transition from a living to a dead cell, e.g. depending on the status of the cell itself (we cannot kill an already dead cell). More importantly, eventually we would like to conditionally kill a cell, depending on the conditions and rules imposed by its neighbours.
Summarizing, we want to map a list of cells to dead cells, given a certain condition (= predicate!).
The specification for the toDeadCell(Predicate<Cell> isCellKillable)
mapping
@Test
void toDeadCellMapping() {
Predicate<Cell> ifCellKillable = isLiving;
Optional<Cell> mappedList =
Optional
.of(livingCell(0, 0))
.map(toDeadCell(ifCellKillable));
assertFalse(mappedList.isEmpty());
assertTrue(isDead(mappedList.get()));
}
And the code that makes this test pass
The implementation for the toDeadCell(Predicate<Cell> isCellKillable)
mapping
public static Function<Cell, Cell> toDeadCell(Predicate<Cell> isCellKillable) {
return cell -> Optional
.of(cell)
.filter(isCellKillable.negate())
.orElse(newDeadCell());
}
Analogously we implement the toLivingCell(Predicate<Cell> isCellViable)
.
As we need to be able to determine the neighbours of a cell, we need
to introduce coordinates in the cell.
Introduction of coordinates in a cell
public class Cell {
private final boolean alive;
private final int x;
private final int y;
private Cell(final int x, final int y, final boolean alive) {
this.alive = alive;
this.x = x;
this.y = y;
}
private boolean isAlive() {
return alive;
}
public static final Cell newLivingCell(final int x, final int y) {
return new Cell(x, y, true);
}
public static final Cell newDeadCell(final int x, final int y) {
return new Cell(x, y, false);
}
public static Predicate<Cell> isLiving = Cell::isAlive;
public static Predicate<Cell> isDead = isLiving.negate();
public static Function<Cell, Cell> toDeadCell(Predicate<Cell> isCellKillable) {
return cell -> Optional
.of(cell)
.filter(isCellKillable.negate())
.orElse(newDeadCell(cell.x, cell.y));
}
public static Function<Cell, Cell> toLivingCell(Predicate<Cell> isCellViable) {
return cell -> Optional
.of(cell)
.filter(isCellViable.negate())
.orElse(newLivingCell(cell.x, cell.y));
}
}
The tests need to be modified accordingly as well, of course!
The rules of the game depend on the number of living neighbours. Consequently, we need to define a predicate that for a given field determines the number of living neighbours in a game.
Let's create a separate test class containing the specifications for the neighbours and tackle the most generic case first, namely a non-edge cell should have eight neighbours.
A non-edge cell should have eight neighbours
class NeighboursTest {
@Test
void filterNeighboursForGivenCenterCell() {
List<Cell> game = List.of(
livingCell(0, 0), livingCell(0, 1), livingCell(0, 2),
livingCell(1, 0), livingCell(1, 1), livingCell(1, 2),
livingCell(2, 0), livingCell(2, 1), livingCell(2, 2)
);
assertEquals(8,
game.stream()
.filter(isNeighbourOf(game.get(4)))
.collect(Collectors.toList())
.size());
}
}
and the simplest thing/solution that could possibly work to make this test pass
Making the test pass
public static Predicate<Cell> isNeighbourOf(final Cell givenCell) {
return cell -> !cell.equals(givenCell);
}
Next, we test for a left-edge cell.
A left-edge cell should have five neighbours
class NeighboursTest {
@Test
void filterNeighboursForGivenLeftEdgeCell() {
List<Cell> game = List.of(
livingCell(0, 0), livingCell(0, 1), livingCell(0, 2),
livingCell(1, 0), livingCell(1, 1), livingCell(1, 2),
livingCell(2, 0), livingCell(2, 1), livingCell(2, 2)
);
assertEquals(5,
game.stream()
.filter(isNeighbourOf(game.get(3)))
.collect(Collectors.toList())
.size());
}
}
and the simplest thing/solution that could possibly work to make this test pass
Making the test pass
public static Predicate<Cell> isNeighbourOf(final Cell givenCell) {
return cell ->
!cell.equals(givenCell) &&
(cell.x - givenCell.x < 2) &&
(cell.y - givenCell.y < 2);
}
Obviously, we have to apply the DRY principle in the tests:
Applying the DRY principle to the tests
class NeighboursTest {
private List<Cell> game;
@BeforeEach
private void setUpGame() {
game = List.of(
livingCell(0, 0), livingCell(0, 1), livingCell(0, 2),
livingCell(1, 0), livingCell(1, 1), livingCell(1, 2),
livingCell(2, 0), livingCell(2, 1), livingCell(2, 2)
);
}
@Test
void filterNeighboursForGivenCenterCell() {
assertEquals(
game.stream()
.filter(isNeighbourOf(game.get(4)))
.collect(Collectors.toList())
.size(), 8);
}
// ...
Now see what happens if we test a right-edge cell.
A right-edge cell should have five neighbours
class NeighboursTest {
@Test
void filterNeighboursForGivenRightEdgeCell() {
List<Cell> game = List.of(
livingCell(0, 0), livingCell(0, 1), livingCell(0, 2),
livingCell(1, 0), livingCell(1, 1), livingCell(1, 2),
livingCell(2, 0), livingCell(2, 1), livingCell(2, 2)
);
assertEquals(5,
game.stream()
.filter(isNeighbourOf(game.get(5)))
.collect(Collectors.toList())
.size());
}
}
We note that this test fails, as the subtraction of the indices may become negative. Note that we only have to apply a fix to the subtraction of the y-coordinates to make the test pass!
Making the test pass
public static Predicate<Cell> isNeighbourOf(final Cell givenCell) {
return cell ->
!cell.equals(givenCell) &&
(cell.x - givenCell.x < 2) &&
(Math.abs(cell.y - givenCell.y) < 2);
}
We can force a similar generalization for the x-coordinate by writing a test for the top-edge cell. As the test and solution are almost identical to the code snippets listed above, this is left as an exercise for the reader.
Ultimately, we are interested in the living neighbours of a cell, given a board. So let's write a specification that defines precisely this feature, namely a function that returns a list of living neighbours of a given cell in a game.
Defining the Function<Cell, List<Cell>> livingNeighboursIn(game)
function
class GameTest {
@Test
void assertNumberOfLivingNeighboursInAGameForAGivenCell() {
List<Cell> game = List.of(
deadCell(0, 0), livingCell(0, 1), livingCell(0, 2),
livingCell(1, 0), livingCell(1, 1), deadCell(1, 2),
deadCell(2, 0), livingCell(2, 1), deadCell(2, 2)
);
assertEquals(livingNeighboursIn(game).apply(game.get(0)).size(), 3);
assertEquals(livingNeighboursIn(game).apply(game.get(1)).size(), 3);
assertEquals(livingNeighboursIn(game).apply(game.get(2)).size(), 2);
assertEquals(livingNeighboursIn(game).apply(game.get(3)).size(), 3);
assertEquals(livingNeighboursIn(game).apply(game.get(4)).size(), 4);
assertEquals(livingNeighboursIn(game).apply(game.get(5)).size(), 4);
assertEquals(livingNeighboursIn(game).apply(game.get(6)).size(), 3);
assertEquals(livingNeighboursIn(game).apply(game.get(7)).size(), 2);
assertEquals(livingNeighboursIn(game).apply(game.get(8)).size(), 2);
}
}
We can easily make this test pass.
Making the test pass
public class Game {
public static Function<Cell, List<Cell>> livingNeighboursIn(final List<Cell> game) {
return cell -> game
.stream()
.filter(isNeighbourOf(cell))
.filter(isLiving)
.collect(Collectors.toList());
}
}
Note that we define this function in a dedicated Game
class with an
associated class containing the tests.
Remember that the rules of the game of life are based on the number of living neighbours:
- A dead cell resurrects if it has exactly three living neighbours
- A living cell dies if it has less than two living neighbours
- A living cell dies if it has more than three living neighbours
The predicates are easily distilled from these game rules: they are written in italics!
Defining the tests for the predicates
@Test
void assertExactlyThreeLivingNeighboursForAGivenCellInAGame() {
List<Cell> game = List.of(
deadCell(0, 0), livingCell(0, 1), livingCell(0, 2),
livingCell(1, 0), livingCell(1, 1), deadCell(1, 2),
deadCell(2, 0), livingCell(2, 1), deadCell(2, 2)
);
assertEquals(4,
game.stream()
.filter(hasExactlyThree(livingNeighboursIn(game)))
.collect(Collectors.toList())
.size()
);
}
The implementation of this predicate is relatively straightforward.
Implementation of the predicate
public static Predicate<Cell> hasExactlyThree(Function<Cell, List<Cell>> findNeighbours) {
return cell -> findNeighbours.apply(cell).size() == 3;
}
Obviously, we should apply the DRY principle once more in the test class, as we have duplicated the set-up of a game.
Finally, the other predicates are implemented analogously, so we don't include them here for the sake of brevity.
Eventually, we want to apply these rules to each cell in a game when going to the next iteration:
- if a cell is dead and has exactly three living neighbours, it should be mapped to a living cell
- if a cell is alive and has less than two living neightbours or more than three living neighbours, it should be mapped to a dead cell
- All other cells should be left unchanged
So we need a means to combine predicates with and and or.
Specification for the or()
predicate
class FunctionalExtensionsTest {
private static final String AAP = "Aap";
private static final String NOOT = "Noot";
private static final String MIES = "Mies";
private static final String WIM = "Wim";
private static final String ZUS = "Zus";
private static final String JET = "Jet";
private static final String FILTER_VALUE = WIM;
private static final List<String> READING_SHELF = List.of(AAP, NOOT, MIES, WIM, ZUS, JET);
private static final Predicate<String> isWim = word -> word.equals(WIM);
private static final Predicate<String> isMies = word -> word.equals(MIES);
@Test
void orBiFunctionCombinesPredicates() {
List<String> filteredList = READING_SHELF
.stream()
.filter(or.apply(isMies, isWim))
.collect(Collectors.toList());
assertEquals(2, filteredList.size());
assertTrue(filteredList.contains(WIM));
assertTrue(filteredList.contains(MIES));
}
And the code that makes the test pass:
Definition of the or()
predicate
public static <T> BiFunction<Predicate<T>, Predicate<T>, Predicate<T>> or() {
return (predicateLeft, predicateRight) -> predicateLeft.or(predicateRight);
}
Analogously we implement the and()
and which()
predicates.
Specification for the and()
and which()
predicates
@Test
void andBiFunctionCombinesPredicates() {
List<String> filteredList = READING_SHELF
.stream()
.filter(and.apply(isMies, isWim))
.collect(Collectors.toList());
assertTrue(filteredList.isEmpty());
}
@Test
void whichFunctionCombinesPredicates() {
List<String> filteredList = READING_SHELF
.stream()
.filter(which(isMies, or, isWim))
.collect(Collectors.toList());
assertEquals(2, filteredList.size());
assertTrue(filteredList.contains(WIM));
assertTrue(filteredList.contains(MIES));
}
And the code that makes the test pass:
Definition of the or()
predicate
public static <T> BiFunction<Predicate<T>, Predicate<T>, Predicate<T>> and() {
return (predicateLeft, predicateRight) -> predicateLeft.and(predicateRight);
}
public static <T> Predicate<T> which(Predicate<T> leftPredicate,
BiFunction<Predicate<T>, Predicate<T>, Predicate<T>> combiner, Predicate<T> rightPredicate) {
return combiner.apply(leftPredicate, rightPredicate);
We have now constructed a domain-specific language with which we can realize the snippet from the teaser listed at the beginning of these instructions!
public static List<Cell> iterateGameboard(final List<Cell> gameboard) {
return gameboard
.stream()
.map(toDeadCell(which(isLiving, and(),
which(hasLessThanTwo(livingNeighboursIn(gameboard)), or(), hasMoreThanThree(livingNeighboursIn(gameboard))))))
.map(toLivingCell(which(isDead, and(), hasExactlyThree(livingNeighboursIn(gameboard)))))
.collect(Collectors.toList());
}
First, we need to be able to create a game-of-life world. And equally important, we need to be able to check (and watch/inspect) it. For example, a blinker oscillator should be something like
-----
--#--
--#--
--#--
-----
Roughly speaking, we may distinguish the following steps:
-
So first of all, a cell should be mapped to either
-
or#
, depending on whether it is alive or not. -
Secondly, this strongly suggests to generate a list of strings (
List<String>
) as output to represent a game board, so we need a mapping fromList<Cell>
→List<String>
. -
As a consequence, it would also be convenient to have a method that initializes a game board by using the same list of strings,
initGame(List<String>)
. -
Finally, we can test your next iteration logic!
Testing the mapping of a cell to a character
@Test
void mapLivingCellToCharacter() {
Optional<String> cellCharacter =
Optional
.of(livingCell(0, 0))
.map(mapToCharacter());
assertEquals("#", cellCharacter.get());
}
@Test
void mapDeadCellToCharacter() {
Optional<String> cellCharacter =
Optional
.of(deadCell(0, 0))
.map(mapToCharacter());
assertEquals("-", cellCharacter.get());
}
The implementation that makes these tests pass is given below.
Mapping a cell to a character depending on its state
public static Function<Cell, String> mapToCharacter() {
return cell -> cell.isAlive() ? "#" : "-";
}
The next challenge is that we are stuck with a list of cells, which is one-dimensional by definition. We somehow need to convert that into a two-dimensional representation.
We'll do so by first creating a map where the keys are the values of the x-coordinates (i.e. the rows), and the values a list of cells that have been mapped to their character representation that we implemented in step 1.
Creating a hash map with rows as keys and a list of cells mapped to chars
Map<Integer, List<String>> rowMap =
board
.stream()
.collect(groupBy(Cell::getX, mapToCharacter()));
Next, we sort the entries of the hashmap by key value (i.e. the x-coordinate), so that the game rows are listed in order. Finally, we convert the list of chars to strings.
Finalizing the board representation
public static List<String> boardRepresentation(final List<Cell> board) {
return board
.stream()
.collect(groupBy(Cell::getX, mapToCharacter()))
.entrySet()
.stream()
.sorted(byYCoordinate())
.map(toSingleLine())
.map(createTextLine())
//.peek(System.out::println)
.collect(Collectors.toList());
}
Note that the line containing the peek()
statement
may be activated to inspect the output at run-time!
Finally, let's address the initialization of a game board.
Specification for the initialization of a new board
class GameTest {
private static final List<String> BLINKER_START_POSITION = List.of(
"-----",
"--#--",
"--#--",
"--#--",
"-----");
// ...
@Test
void createWorldWithBlinkerOscillator() {
List<Cell> gameboard = initGame(BLINKER_START_POSITION);
assertEquals(BLINKER_START_POSITION, boardRepresentation(gameboard));
}
The implementation of the initGame(List<String>)
method
public static List<Cell> initGame(final List<String> initialState) {
List<Cell> game = new LinkedList<Cell>();
for (int x = 0; x < initialState.size(); x++)
for (int y = 0; y < initialState.get(x).length(); y++)
game.add( initialState.get(x).charAt(y) == '#' ? livingCell(x, y) : deadCell(x,y));
return game;
}
Specification for the initialization of a new board
class GameTest {
// ...
private static final List<String> BLINKER_END_POSITION = List.of(
"-----",
"--#--",
"--#--",
"--#--",
"-----");
// ...
@Test
void iterateWorldWithBlinkerOscillator() {
List<Cell> gameboard = initGame(BLINKER_START_POSITION);
gameboard = iterateGameboard(gameboard);
assertEquals(BLINKER_END_POSITION, boardRepresentation(gameboard));
}
Finally testing the iteration logic from the teaser!
public static List<Cell> iterateGameboard(final List<Cell> gameboard) {
return gameboard
.stream()
.map(toDeadCell(which(isLiving, and(),
which(hasLessThanTwo(livingNeighboursIn(gameboard)), or(), hasMoreThanThree(livingNeighboursIn(gameboard))))))
.map(toLivingCell(which(isDead, and(), hasExactlyThree(livingNeighboursIn(gameboard)))))
.collect(Collectors.toList());
}