diff --git a/55_Life/java/README.md b/55_Life/java/README.md index 51edd8d4..19a5ff38 100644 --- a/55_Life/java/README.md +++ b/55_Life/java/README.md @@ -1,3 +1,19 @@ +# Game of Life - Java version + Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Oracle Java](https://openjdk.java.net/) + +## Requirements + +* Requires Java 17 (or later) + +## Notes + +The Java version of Game of Life tries to mimics the behaviour of the BASIC version. +However, the Java code does not have much in common with the original. + +**Differences in behaviour:** +* Input supports the ```.``` character, but it's optional. +* Evaluation of ```DONE``` input string is case insensitive. +* Run with the ```-s``` command line argument to halt the program after each generation, and continue when ```ENTER``` is pressed. \ No newline at end of file diff --git a/55_Life/java/src/java/Life.java b/55_Life/java/src/java/Life.java new file mode 100644 index 00000000..2f5c184a --- /dev/null +++ b/55_Life/java/src/java/Life.java @@ -0,0 +1,191 @@ +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * The Game of Life class.
+ *
+ * Mimics the behaviour of the BASIC version, however the Java code does not have much in common with the original. + *
+ * Differences in behaviour: + * + */ +public class Life { + + private static final byte DEAD = 0; + private static final byte ALIVE = 1; + private static final String NEWLINE = "\n"; + + private final Scanner consoleReader = new Scanner(System.in); + + private final byte[][] matrix = new byte[21][67]; + private int generation = 0; + private int population = 0; + boolean stopAfterGen = false; + boolean invalid = false; + + /** + * Constructor. + * + * @param args the command line arguments + */ + public Life(String[] args) { + parse(args); + } + + private void parse(String[] args) { + for (String arg : args) { + if ("-s".equals(arg)) { + stopAfterGen = true; + break; + } + } + } + + /** + * Starts the game. + */ + public void start() { + printGameHeader(); + readPattern(); + while (true) { + printGeneration(); + advanceToNextGeneration(); + if (stopAfterGen) { + System.out.print("PRESS ENTER TO CONTINUE"); + consoleReader.nextLine(); + } + } + } + + private void advanceToNextGeneration() { + // store all cell transitions in a list, i.e. if a dead cell becomes alive, or a living cell dies + List transitions = new ArrayList<>(); + // there's still room for optimization: instead of iterating over all cells in the matrix, + // we could consider only the section containing the pattern(s), as in the BASIC version + for (int y = 0; y < matrix.length; y++) { + for (int x = 0; x < matrix[y].length; x++) { + int neighbours = countNeighbours(y, x); + if (matrix[y][x] == ALIVE) { + if (neighbours < 2 || neighbours > 3) { + transitions.add(new Transition(y, x, DEAD)); + population--; + } + } else { // cell is dead + if (neighbours == 3) { + if (x < 2 || x > 67 || y < 2 || y > 21) { + invalid = true; + } + transitions.add(new Transition(y, x, ALIVE)); + population++; + } + } + } + } + // apply all transitions to the matrix + transitions.forEach(t -> matrix[t.y()][t.x()] = t.newState()); + generation++; + } + + private int countNeighbours(int y, int x) { + int neighbours = 0; + for (int row = Math.max(y - 1, 0); row <= Math.min(y + 1, matrix.length - 1); row++) { + for (int col = Math.max(x - 1, 0); col <= Math.min(x + 1, matrix[row].length - 1); col++) { + if (row == y && col == x) { + continue; + } + if (matrix[row][col] == ALIVE) { + neighbours++; + } + } + } + return neighbours; + } + + private void readPattern() { + System.out.println("ENTER YOUR PATTERN:"); + List lines = new ArrayList<>(); + String line; + int maxLineLength = 0; + boolean reading = true; + while (reading) { + System.out.print("? "); + line = consoleReader.nextLine(); + if (line.equalsIgnoreCase("done")) { + reading = false; + } else { + // optional support for the '.' that is needed in the BASIC version + lines.add(line.replace('.', ' ')); + maxLineLength = Math.max(maxLineLength, line.length()); + } + } + fillMatrix(lines, maxLineLength); + } + + private void fillMatrix(List lines, int maxLineLength) { + float xMin = 33 - maxLineLength / 2f; + float yMin = 11 - lines.size() / 2f; + for (int y = 0; y < lines.size(); y++) { + String line = lines.get(y); + for (int x = 1; x <= line.length(); x++) { + if (line.charAt(x-1) == '*') { + matrix[floor(yMin + y)][floor(xMin + x)] = ALIVE; + population++; + } + } + } + } + + private int floor(float f) { + return (int) Math.floor(f); + } + + private void printGameHeader() { + printIndented(34, "LIFE"); + printIndented(15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + System.out.println(NEWLINE.repeat(3)); + } + + private void printIndented(int spaces, String str) { + System.out.println(" ".repeat(spaces) + str); + } + + private void printGeneration() { + printGenerationHeader(); + for (int y = 0; y < matrix.length; y++) { + for (int x = 0; x < matrix[y].length; x++) { + System.out.print(matrix[y][x] == 1 ? "*" : " "); + } + System.out.println(); + } + } + + private void printGenerationHeader() { + String invalidText = invalid ? "INVALID!" : ""; + System.out.printf("GENERATION: %-13d POPULATION: %d %s\n", generation, population, invalidText); + } + + /** + * Main method that starts the program. + * + * @param args the command line arguments: + *
-s: Stop after each generation (press enter to continue)
+ * @throws Exception if something goes wrong. + */ + public static void main(String[] args) throws Exception { + new Life(args).start(); + } + +} + +/** + * Represents a state change for a single cell within the matrix. + * + * @param y the y coordinate (row) of the cell + * @param x the x coordinate (column) of the cell + * @param newState the new state of the cell (either DEAD or ALIVE) + */ +record Transition(int y, int x, byte newState) { }