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 index b8cff6adc6a5d5da330de97001da6ebc35b5b025..91e04656bd13052e94e6ea0ac76b0dbc03370d23 100644 --- a/game-of-life/src/main/java/fr/ippon/life/Cell.java +++ b/game-of-life/src/main/java/fr/ippon/life/Cell.java @@ -2,24 +2,16 @@ package fr.ippon.life; import java.util.Set; -public class Cell { - +public record Cell(int row, int column) { 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; + return neighbourgCount == LOW_POPULATION_THRESHOLD + || neighbourgCount == HIGH_POPULATION_THRESHOLD; } boolean born(Set<Cell> aliveCells) { @@ -27,7 +19,9 @@ public class Cell { } private long countNeigbours(Set<Cell> aliveCells) { - return aliveCells.stream().filter(this::isNeighbour).count(); + return aliveCells.stream() + .filter(this::isNeighbour) + .count(); } private boolean isNeighbour(Cell other) { @@ -35,38 +29,11 @@ public class Cell { return false; } - return delta(row, other.row) <= 1 && delta(column, other.column) <= 1; + 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/Grid.java b/game-of-life/src/main/java/fr/ippon/life/Grid.java index aef8aa67c7462469a87d0f01fd7834c6d47f9010..92ed44436563dc2fde13fcc9f9f00cb14c983465 100644 --- a/game-of-life/src/main/java/fr/ippon/life/Grid.java +++ b/game-of-life/src/main/java/fr/ippon/life/Grid.java @@ -9,55 +9,77 @@ import java.util.stream.Stream; public class Grid { private final Set<Cell> deadCells; - private final Box box; + private final Box border; - Grid(Set<Cell> cells) { - box = new Box(cells); - this.deadCells = buildDeadCells(cells); + Grid(Set<Cell> aliveCells) { + this.deadCells = buildDeadCells(aliveCells); + border = new Box(aliveCells); } - private Set<Cell> buildDeadCells(Set<Cell> cells) { - return IntStream - .range(box.getTopRow(), box.getBottomRow()) - .mapToObj(Integer::valueOf) - .flatMap(row -> toDeadCell(cells, row)) + private Set<Cell> buildDeadCells(Set<Cell> aliveCells) { + return aliveCells + .stream() + .map(Box::new) + .flatMap(Box::cells) + .filter(cell -> !aliveCells.contains(cell)) .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; } + public int leftColumn() { + return border.leftColumn(); + } + + public int rightColumn() { + return border.rightColumn(); + } + + public int topRow() { + return border.topRow(); + } + + public int bottomRow() { + return border.bottomRow(); + } + private static class Box { private final Point topLeft; private final Point bottomRight; + public Box(Cell cell) { + this(Set.of(cell)); + } + public Box(Set<Cell> cells) { topLeft = Point.topLeft(cells); bottomRight = Point.bottomRight(cells); } - public int getTopRow() { + private Stream<Cell> cells() { + return IntStream.range(leftColumn(), rightColumn()).mapToObj(Integer::valueOf).flatMap(column -> cellsColumn(column)); + } + + private Stream<Cell> cellsColumn(Integer column) { + return IntStream.range(topRow(), bottomRow()).mapToObj(Integer::valueOf).map(row -> new Cell(row, column)); + } + + public int topRow() { return topLeft.getRow(); } - public int getBottomRow() { + public int bottomRow() { return bottomRight.getRow() + 1; } - public int getLeftColumn() { + public int leftColumn() { return topLeft.getColumn(); } - public int getRightColumn() { + public int rightColumn() { return bottomRight.getColumn() + 1; } } @@ -73,17 +95,17 @@ public class Grid { } private static Point topLeft(Set<Cell> cells) { - int row = cells.stream().mapToInt(Cell::getRow).min().orElse(0); + int row = cells.stream().mapToInt(Cell::row).min().orElse(0); - int column = cells.stream().mapToInt(Cell::getColumn).min().orElse(0); + int column = cells.stream().mapToInt(Cell::column).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 row = cells.stream().mapToInt(Cell::row).max().orElse(0); - int column = cells.stream().mapToInt(Cell::getColumn).max().orElse(0); + int column = cells.stream().mapToInt(Cell::column).max().orElse(0); return new Point(row + 1, column + 1); } diff --git a/game-of-life/src/main/java/fr/ippon/life/Renderer.java b/game-of-life/src/main/java/fr/ippon/life/Renderer.java new file mode 100644 index 0000000000000000000000000000000000000000..ef99786fed395a03bdba78e0699978e862bba3a2 --- /dev/null +++ b/game-of-life/src/main/java/fr/ippon/life/Renderer.java @@ -0,0 +1,5 @@ +package fr.ippon.life; + +public interface Renderer<T> { + T run(Grid grid); +} diff --git a/game-of-life/src/main/java/fr/ippon/life/StringRenderer.java b/game-of-life/src/main/java/fr/ippon/life/StringRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..03a602d2d405ac0a2166063fc9bd3e76cda1de8c --- /dev/null +++ b/game-of-life/src/main/java/fr/ippon/life/StringRenderer.java @@ -0,0 +1,48 @@ +package fr.ippon.life; + +import java.util.function.IntFunction; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class StringRenderer implements Renderer<String> { + + @Override + public String run(Grid grid) { + return new GridRenderer(grid).run(); + } + + private static class GridRenderer { + + private final Grid grid; + + private GridRenderer(Grid grid) { + this.grid = grid; + } + + private String run() { + return IntStream + .range(grid.topRow(), grid.bottomRow()) + .mapToObj(Integer::valueOf) + .map(row -> rowRepresentation(grid, row)) + .collect(Collectors.joining(System.lineSeparator(), "", System.lineSeparator())); + } + + private String rowRepresentation(Grid grid, Integer row) { + return IntStream.range(grid.leftColumn(), grid.rightColumn()).mapToObj(cellRepresentation(row)).collect(Collectors.joining()); + } + + private IntFunction<String> cellRepresentation(Integer row) { + return column -> { + if (aliveCell(row, column)) { + return "o"; + } + + return "x"; + }; + } + + private boolean aliveCell(Integer row, int column) { + return !grid.deadCells().contains(new Cell(row, 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 index 799cb41e0666d05ccefa2cb0484afdbf88056af0..8c09185a12d289b18c2b72abed2a1929df347a1d 100644 --- a/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java +++ b/game-of-life/src/test/java/fr/ippon/life/GameOfLifeTest.java @@ -39,20 +39,20 @@ class GameOfLifeTest { @Test void nextGenerationShouldHaveOneColumnFromOneLine() { - Generation nextGeneration = new Generation(cell(4, 5), cell(5, 5), cell(6, 5)).next(); + Generation nextGeneration = threeCellsColumn().next(); assertThat(nextGeneration.getAliveCells()).containsExactlyInAnyOrder(cell(5, 4), cell(5, 5), cell(5, 6)); } @Test void shouldAdvanceInGenerations() { - Generation nextGeneration = threeCellsColumn(); + Generation nextGeneration = threeCellsColumn().next().next(); 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(); + return new Generation(cell(4, 5), cell(5, 5), cell(6, 5)); } private static Cell cell(int row, int column) { diff --git a/game-of-life/src/test/java/fr/ippon/life/StringRendererUnitTest.java b/game-of-life/src/test/java/fr/ippon/life/StringRendererUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5afdb183b19accff6ae519d75ee5f3a8092065ad --- /dev/null +++ b/game-of-life/src/test/java/fr/ippon/life/StringRendererUnitTest.java @@ -0,0 +1,47 @@ +package fr.ippon.life; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class StringRendererUnitTest { + + private static final StringRenderer renderer = new StringRenderer(); + + @Test + void shouldDisplayGridWithOneAliveCell() { + assertThat(renderer.run(grid().alive(1, 1).build())).isEqualTo(join("xxx", "xox", "xxx")); + } + + @Test + void shouldDisplayGridWithTwoAliveCell() { + assertThat(renderer.run(grid().alive(1, 1).alive(1, 3).build())).isEqualTo(join("xxxxx", "xoxox", "xxxxx")); + } + + private static String join(String... parts) { + return Arrays.stream(parts).collect(Collectors.joining(System.lineSeparator(), "", System.lineSeparator())); + } + + private GridBuilder grid() { + return new GridBuilder(); + } + + private static class GridBuilder { + + private final Set<Cell> cells = new HashSet<>(); + + public GridBuilder alive(int row, int column) { + cells.add(new Cell(row, column)); + + return this; + } + + public Grid build() { + return new Grid(cells); + } + } +} diff --git a/package-lock.json b/package-lock.json index b9bb3235d71a75522bb91f16e439272a50015ec7..c5cef87e66a62a5dbf8dc3a86d35d398efc04181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1276,8 +1276,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -2093,6 +2092,66 @@ "java-parser": "1.0.2", "lodash": "4.17.21", "prettier": "2.2.1" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } } }, "pretty-quick": {