From 04b65a9f1ab7d08a5ff8ad1c345b0cfc8d6bc487 Mon Sep 17 00:00:00 2001
From: Colin DAMON <cdamon@ippon.fr>
Date: Thu, 21 Jan 2021 08:44:14 +0100
Subject: [PATCH] Game of life implementation

---
 .../src/main/java/fr/ippon/life/Cell.java     | 71 ++++++++++++++
 .../main/java/fr/ippon/life/Generation.java   | 36 +++++++
 .../src/main/java/fr/ippon/life/Grid.java     | 96 +++++++++++++++++++
 .../java/fr/ippon/life/GameOfLifeTest.java    | 61 ++++++++++++
 4 files changed, 264 insertions(+)
 create mode 100644 game-of-life/src/main/java/fr/ippon/life/Cell.java
 create mode 100644 game-of-life/src/main/java/fr/ippon/life/Generation.java
 create mode 100644 game-of-life/src/main/java/fr/ippon/life/Grid.java
 create mode 100644 game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java

diff --git a/game-of-life/src/main/java/fr/ippon/life/Cell.java b/game-of-life/src/main/java/fr/ippon/life/Cell.java
new file mode 100644
index 00000000..9fdfad94
--- /dev/null
+++ b/game-of-life/src/main/java/fr/ippon/life/Cell.java
@@ -0,0 +1,71 @@
+package fr.ippon.life;
+
+import java.util.Set;
+
+public class Cell {
+  private static final int LOW_POPULATION_THRESHOLD = 2;
+  private static final int HIGH_POPULATION_THRESHOLD = 3;
+  private static final int BORN_THRESHOLD = HIGH_POPULATION_THRESHOLD;
+
+  private final int row;
+  private final int column;
+
+  public Cell(int row, int column) {
+    this.row = row;
+    this.column = column;
+  }
+
+  boolean stayAlive(Set<Cell> aliveCells) {
+    long neighbourgCount = countNeigbours(aliveCells);
+
+    return neighbourgCount == LOW_POPULATION_THRESHOLD || neighbourgCount == HIGH_POPULATION_THRESHOLD;
+  }
+
+  boolean born(Set<Cell> aliveCells) {
+    return countNeigbours(aliveCells) == BORN_THRESHOLD;
+  }
+
+  private long countNeigbours(Set<Cell> aliveCells) {
+    return aliveCells.stream().filter(this::isNeighbour).count();
+  }
+
+  private boolean isNeighbour(Cell other) {
+    if (other.equals(this)) {
+      return false;
+    }
+
+    return delta(row, other.row) <= 1 && delta(column, other.column) <= 1;
+  }
+
+  private static int delta(int first, int second) {
+    return Math.abs(first - second);
+  }
+
+  public int getRow() {
+    return row;
+  }
+
+  public int getColumn() {
+    return column;
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + column;
+    result = prime * result + row;
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (obj == null) return false;
+    if (getClass() != obj.getClass()) return false;
+    Cell other = (Cell) obj;
+    if (column != other.column) return false;
+    if (row != other.row) return false;
+    return true;
+  }
+}
diff --git a/game-of-life/src/main/java/fr/ippon/life/Generation.java b/game-of-life/src/main/java/fr/ippon/life/Generation.java
new file mode 100644
index 00000000..d314bfe5
--- /dev/null
+++ b/game-of-life/src/main/java/fr/ippon/life/Generation.java
@@ -0,0 +1,36 @@
+package fr.ippon.life;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Generation {
+  private Set<Cell> aliveCells;
+
+  public Generation(Cell... aliveCells) {
+    this(Set.of(aliveCells));
+  }
+
+  private Generation(Set<Cell> cells) {
+    aliveCells = cells;
+  }
+
+  public Generation next() {
+    Stream<Cell> stayAlive = aliveCells.stream().filter(cell -> cell.stayAlive(aliveCells));
+
+    Stream<Cell> born = buildGrid().deadCells().stream().filter(cell -> cell.born(aliveCells));
+
+    Set<Cell> cells = Stream.concat(stayAlive, born).collect(Collectors.toUnmodifiableSet());
+
+    return new Generation(cells);
+  }
+
+  public Collection<Cell> getAliveCells() {
+    return aliveCells;
+  }
+
+  private Grid buildGrid() {
+    return new Grid(aliveCells);
+  }
+}
diff --git a/game-of-life/src/main/java/fr/ippon/life/Grid.java b/game-of-life/src/main/java/fr/ippon/life/Grid.java
new file mode 100644
index 00000000..fc866905
--- /dev/null
+++ b/game-of-life/src/main/java/fr/ippon/life/Grid.java
@@ -0,0 +1,96 @@
+package fr.ippon.life;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+public class Grid {
+  private final Set<Cell> deadCells;
+  private final Box box;
+
+  Grid(Set<Cell> cells) {
+    box = new Box(cells);
+    this.deadCells = buildDeadCells(cells);
+  }
+
+  private Set<Cell> buildDeadCells(Set<Cell> cells) {
+    return IntStream
+      .range(box.getTopRow(), box.getBottomRow())
+      .mapToObj(Integer::valueOf)
+      .flatMap(row -> toDeadCell(cells, row))
+      .collect(Collectors.toUnmodifiableSet());
+  }
+
+  private Stream<Cell> toDeadCell(Set<Cell> cells, int row) {
+    return IntStream
+      .range(box.getLeftColumn(), box.getRightColumn())
+      .mapToObj(column -> new Cell(row, column))
+      .filter(cell -> !cells.contains(cell));
+  }
+
+  public Collection<Cell> deadCells() {
+    return deadCells;
+  }
+
+  private static class Box {
+    private final Point topLeft;
+    private final Point bottomRight;
+
+    public Box(Set<Cell> cells) {
+      topLeft = Point.topLeft(cells);
+      bottomRight = Point.bottomRight(cells);
+    }
+
+    public int getTopRow() {
+      return topLeft.getRow();
+    }
+
+    public int getBottomRow() {
+      return bottomRight.getRow() + 1;
+    }
+
+    public int getLeftColumn() {
+      return topLeft.getColumn();
+    }
+
+    public int getRightColumn() {
+      return bottomRight.getColumn() + 1;
+    }
+  }
+
+  private static class Point {
+    private final int row;
+    private final int column;
+
+    public Point(int row, int column) {
+      this.row = row;
+      this.column = column;
+    }
+
+    private static Point topLeft(Set<Cell> cells) {
+      int row = cells.stream().mapToInt(Cell::getRow).min().orElse(0);
+
+      int column = cells.stream().mapToInt(Cell::getColumn).min().orElse(0);
+
+      return new Point(row - 1, column - 1);
+    }
+
+    private static Point bottomRight(Set<Cell> cells) {
+      int row = cells.stream().mapToInt(Cell::getRow).max().orElse(0);
+
+      int column = cells.stream().mapToInt(Cell::getColumn).max().orElse(0);
+
+      return new Point(row + 1, column + 1);
+    }
+
+    public int getRow() {
+      return row;
+    }
+
+    public int getColumn() {
+      return column;
+    }
+  }
+}
diff --git a/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java b/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java
new file mode 100644
index 00000000..799cb41e
--- /dev/null
+++ b/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java
@@ -0,0 +1,61 @@
+package fr.ippon.life;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class GameOfLifeTest {
+
+  @Test
+  void nextGenerationShouldHaveAllDeadCellsForAllDeadGrid() {
+    Generation nextGeneration = new Generation().next();
+
+    assertThat(nextGeneration.getAliveCells()).isEmpty();
+  }
+
+  @Test
+  void nextGenerationShouldHaveAllDeadCellsFromOneAliveCell() {
+    Generation nextGeneration = new Generation(cell(1, 1)).next();
+
+    assertThat(nextGeneration.getAliveCells()).isEmpty();
+  }
+
+  @Test
+  void nextGenerationShouldHaveAllDeadCellsFromTwoContiguousAliveCells() {
+    Generation nextGeneration = new Generation(cell(1, 1), cell(2, 1)).next();
+
+    assertThat(nextGeneration.getAliveCells()).isEmpty();
+  }
+
+  @Test
+  void nextGenerationShouldHaveFourAliveCellsFromTriangularConfiguration() {
+    Cell first = cell(1, 2);
+    Cell second = cell(2, 1);
+    Cell third = cell(2, 2);
+    Generation nextGeneration = new Generation(first, second, third).next();
+
+    assertThat(nextGeneration.getAliveCells()).containsExactlyInAnyOrder(cell(1, 1), first, second, third);
+  }
+
+  @Test
+  void nextGenerationShouldHaveOneColumnFromOneLine() {
+    Generation nextGeneration = new Generation(cell(4, 5), cell(5, 5), cell(6, 5)).next();
+
+    assertThat(nextGeneration.getAliveCells()).containsExactlyInAnyOrder(cell(5, 4), cell(5, 5), cell(5, 6));
+  }
+
+  @Test
+  void shouldAdvanceInGenerations() {
+    Generation nextGeneration = threeCellsColumn();
+
+    assertThat(nextGeneration.getAliveCells()).containsExactlyInAnyOrder(cell(4, 5), cell(5, 5), cell(6, 5));
+  }
+
+  private Generation threeCellsColumn() {
+    return new Generation(cell(4, 5), cell(5, 5), cell(6, 5)).next().next();
+  }
+
+  private static Cell cell(int row, int column) {
+    return new Cell(row, column);
+  }
+}
-- 
GitLab