366 lines
13 KiB
Java
366 lines
13 KiB
Java
import javafx.scene.input.KeyCode;
|
|
|
|
import java.io.*;
|
|
import java.util.Arrays;
|
|
import java.util.Scanner;
|
|
import java.util.Random;
|
|
import java.util.ArrayList;
|
|
|
|
/**
|
|
* Game of life class.
|
|
*
|
|
* @author rasmus
|
|
* @version 4.2
|
|
*/
|
|
public class GameOfLifeMain {
|
|
/**
|
|
* The Is periodic.
|
|
*/
|
|
static boolean isPeriodic = false;
|
|
/**
|
|
* The Auto run.
|
|
*/
|
|
static boolean autoRun = false;
|
|
/**
|
|
* The Key pressed.
|
|
*/
|
|
static boolean keyPressed = false;
|
|
/**
|
|
* The Board size.
|
|
*/
|
|
static int boardSize;
|
|
/**
|
|
* The Text position.
|
|
*/
|
|
static double[] textPosition;
|
|
/**
|
|
* The Jump to iteration.
|
|
*/
|
|
static int jumpToIteration = -1; // When it's < 0, it is to be ignored.
|
|
|
|
/**
|
|
* The entry point of application.
|
|
*
|
|
* @param args the input arguments
|
|
* @throws IOException the io exception
|
|
*/
|
|
public static void main(String[] args) throws IOException {
|
|
StdDraw.setCanvasSize(1000, 1050);
|
|
Scanner console = new Scanner(System.in);
|
|
System.out.print("Please enter the size of the board (enter -1 to input file that should be imported): ");
|
|
boardSize = console.nextInt();
|
|
GameOfLife gameOfLife = new GameOfLife(Math.max(boardSize, 1));
|
|
if (boardSize < 0) {
|
|
System.out.print("Please enter path of file: ");
|
|
String filePath = console.next();
|
|
File file = new File(filePath);
|
|
gameOfLife = importFromFile(file);
|
|
boardSize = gameOfLife.size;
|
|
}
|
|
textPosition = new double[] {10.0 / 1000 * boardSize,(Math.ceil(boardSize * 1.05)-boardSize)/2+boardSize};
|
|
StdDraw.setPenRadius(1.3 / (boardSize + 1));
|
|
StdDraw.show(0);
|
|
StdDraw.setXscale(-1, boardSize);
|
|
StdDraw.setYscale(-1, (int) Math.ceil(boardSize*1.05));
|
|
StdDraw.clear();
|
|
gameOfLife.draw();
|
|
StdDraw.textLeft(textPosition[0], textPosition[1], "Iteration nr.: " + gameOfLife.iteration + " Period is not found yet.");
|
|
StdDraw.show(0);
|
|
while (true) {
|
|
if (!keyPressed && jumpToIteration < 0) {
|
|
if (StdDraw.isKeyPressed(39)) { // Right arrow key is pressed. Go forward 1 iteration
|
|
runIteration(gameOfLife);
|
|
keyPressed = true;
|
|
} else if (StdDraw.isKeyPressed(37) && gameOfLife.iteration > 0) { // Left arrow key is pressed. Go back 1 iteration
|
|
gameOfLife.previousState();
|
|
StdDraw.clear();
|
|
gameOfLife.draw();
|
|
StdDraw.textLeft(textPosition[0], textPosition[1], "Iteration nr.: " + gameOfLife.iteration +
|
|
(isPeriodic ? " Period is: " + gameOfLife.period : " Period is not found yet"));
|
|
StdDraw.show(0);
|
|
keyPressed = true;
|
|
} else if (StdDraw.isKeyPressed(32)) { // If space is pressed. Autoruns or pauses game
|
|
autoRun = !autoRun;
|
|
keyPressed = true;
|
|
} else if (StdDraw.isKeyPressed(73)) { // If "i" is pressed. Goes to inputted iteration.
|
|
/*
|
|
When a iteration nr. is given to be jumped to. The simulation will run in the background not updating the board
|
|
until it reaches that iteration, in which case it will pause running and update board again.
|
|
*/
|
|
System.out.print("Enter iteration to jump to: ");
|
|
jumpToIteration = Integer.parseInt(console.nextLine());
|
|
StdDraw.setPenRadius(50);
|
|
StdDraw.text((double) (boardSize + 1) /2, (double) ((int) Math.ceil(boardSize * 1.05) + 1) /2, "RUNNING SIMULATION. WAIT");
|
|
StdDraw.show(0);
|
|
StdDraw.setPenRadius(1.3 / (boardSize + 1));
|
|
autoRun = true;
|
|
keyPressed = true;
|
|
} else if (StdDraw.isKeyPressed(83)) { // If "s" is pressed. Saves board
|
|
System.out.print("Enter filename (and path): ");
|
|
String savedFile = console.next();
|
|
gameOfLife.exportStateToFile(savedFile);
|
|
}
|
|
} else if (!StdDraw.isKeyPressed(39) && !StdDraw.isKeyPressed(37) && !StdDraw.isKeyPressed(32) && !StdDraw.isKeyPressed(73)) {
|
|
// This is to make sure, that when a key is pressed, it is only acted upon once until it is pressed again.
|
|
keyPressed = false;
|
|
}
|
|
if (autoRun) {
|
|
runIteration(gameOfLife);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run iteration.
|
|
*
|
|
* @param gameOfLife the game of life
|
|
*/
|
|
public static void runIteration(GameOfLife gameOfLife) {
|
|
gameOfLife.nextState();
|
|
int periodic = (isPeriodic ? -1 : gameOfLife.checkPeriodic()); // Only check if board is periodic if it hasnt already been found to be.
|
|
if (periodic >= 0) {
|
|
int period = gameOfLife.iteration - periodic;
|
|
System.out.println("The simulation is periodic. The period is: " + period);
|
|
isPeriodic = true;
|
|
autoRun = gameOfLife.iteration <= jumpToIteration; // Game should autoRun if iteration nr. is below iteration nr. it should run until.
|
|
}
|
|
if (gameOfLife.iteration >= jumpToIteration) {
|
|
StdDraw.clear();
|
|
gameOfLife.draw();
|
|
StdDraw.textLeft(textPosition[0], textPosition[1], "Iteration nr.: " + gameOfLife.iteration +
|
|
(isPeriodic ? " Period is: " + gameOfLife.period : " Period is not found yet"));
|
|
StdDraw.show(0);
|
|
if (gameOfLife.iteration == jumpToIteration) {
|
|
jumpToIteration = -1;
|
|
autoRun = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Import from file game of life.
|
|
*
|
|
* @param file the file
|
|
* @return the game of life
|
|
* @throws FileNotFoundException the file not found exception
|
|
*/
|
|
public static GameOfLife importFromFile(File file) throws FileNotFoundException {
|
|
/*
|
|
Imports a simulation-state from a file with example file format:
|
|
0 1 0 0
|
|
1 0 0 0
|
|
1 1 1 1
|
|
0 0 1 1
|
|
First goes through first line of file (and defines length) and the rest of the lines. Returns a GameOfLife object with imported board.
|
|
*/
|
|
Scanner importedFile = new Scanner(file);
|
|
String firstLine = importedFile.nextLine();
|
|
String[] firstLineSplit = firstLine.split(" ");
|
|
int length = firstLineSplit.length;
|
|
int[][] grid = new int[length][length];
|
|
for (int i = 0; i < length; i++) {
|
|
grid[0][i] = Integer.parseInt(firstLineSplit[i]);
|
|
}
|
|
int rowNum = 1;
|
|
while (importedFile.hasNextLine()) {
|
|
String Line = importedFile.nextLine();
|
|
String[] lineSplit = Line.split(" ");
|
|
for (int i = 0; i < length; i++) {
|
|
grid[rowNum][i] = Integer.parseInt(lineSplit[i]);
|
|
}
|
|
rowNum++;
|
|
}
|
|
importedFile.close();
|
|
return new GameOfLife(grid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The type Game of life.
|
|
*/
|
|
class GameOfLife {
|
|
private int[][] state;
|
|
/**
|
|
* The Size.
|
|
*/
|
|
int size;
|
|
/**
|
|
* The Iteration.
|
|
*/
|
|
int iteration = 0;
|
|
/**
|
|
* The Period.
|
|
*/
|
|
int period = -1;
|
|
/**
|
|
* The First periodic state.
|
|
*/
|
|
int firstPeriodicState; // The index of the state when the simulations periodic nature begins.
|
|
/**
|
|
* The Previous boards list.
|
|
*/
|
|
ArrayList<int[][]> previousBoardsList = new ArrayList<>(); // A list containing all the previous arrays of this.state.
|
|
/**
|
|
* The Previous boards list hash.
|
|
*/
|
|
ArrayList<Integer> previousBoardsListHash = new ArrayList<>(); // A list containing all the previous array-hashes of this.state. Both lists used for checking if periodic
|
|
|
|
/**
|
|
* Instantiates a new Game of life.
|
|
*
|
|
* @param n the n
|
|
*/
|
|
public GameOfLife(int n) {
|
|
/*
|
|
Create a random board of 1's and 0's based on array size n * n
|
|
*/
|
|
Random rand = new Random();
|
|
this.size = n;
|
|
this.state = new int[n][n];
|
|
for (int i = 0; i < n; i++) {
|
|
for (int j = 0; j < n; j++) {
|
|
this.state[i][j] = rand.nextInt(2);
|
|
}
|
|
}
|
|
this.previousBoardsList.add(this.state);
|
|
this.previousBoardsListHash.add(Arrays.deepHashCode(this.state));
|
|
}
|
|
|
|
/**
|
|
* Instantiates a new Game of life.
|
|
*
|
|
* @param initialState the initial state
|
|
*/
|
|
public GameOfLife(int[][] initialState) {
|
|
this.state = initialState;
|
|
this.size = initialState.length;
|
|
this.previousBoardsList.add(this.state);
|
|
this.previousBoardsListHash.add(Arrays.deepHashCode(this.state));
|
|
}
|
|
|
|
public String toString() {
|
|
return Arrays.deepToString(this.state);
|
|
}
|
|
|
|
/**
|
|
* Draw.
|
|
*/
|
|
public void draw() {
|
|
for (int i = 0; i < state.length; i++) {
|
|
for (int j = 0; j < state[i].length; j++) {
|
|
if (this.state[i][j] == 1) {
|
|
StdDraw.point(j, this.size-i-1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Next state.
|
|
*/
|
|
public void nextState() {
|
|
/*
|
|
Goes through the current state-array and builds/calculates next state. Also adds current state and state-hash to lists
|
|
*/
|
|
int[][] newState = new int[this.size][this.size];
|
|
for (int i = 0; i < state.length; i++) {
|
|
for (int j = 0; j < state[i].length; j++) {
|
|
int aliveNeighbors = this.aliveNeighbours(i, j);
|
|
if (this.state[i][j] == 1 && (aliveNeighbors < 2 || aliveNeighbors > 3)) { // If current cell is alive, and it should die of boredom or overcrowding
|
|
newState[i][j] = 0;
|
|
} else if (this.state[i][j] == 0 && aliveNeighbors == 3) { // Dead cell with 3 neighbors returns to life
|
|
newState[i][j] = 1;
|
|
} else {
|
|
newState[i][j] = this.state[i][j];
|
|
}
|
|
}
|
|
}
|
|
this.previousBoardsList.add(this.state);
|
|
this.previousBoardsListHash.add(Arrays.deepHashCode(this.state));
|
|
this.state = newState;
|
|
this.iteration++;
|
|
}
|
|
|
|
/**
|
|
* Previous state.
|
|
*/
|
|
public void previousState() {
|
|
/*
|
|
Makes current state the previous one, and removes it from the lists
|
|
*/
|
|
this.state = this.previousBoardsList.getLast();
|
|
this.previousBoardsList.removeLast();
|
|
this.previousBoardsListHash.removeLast();
|
|
this.iteration--;
|
|
}
|
|
|
|
/**
|
|
* Alive neighbours int.
|
|
*
|
|
* @param x the x
|
|
* @param y the y
|
|
* @return the int
|
|
*/
|
|
public int aliveNeighbours(int x, int y) {
|
|
/*
|
|
Returns the amount of alive neighbors. It does this by adding the values of the surrounding squares.
|
|
In easier to read format, its built like this:
|
|
If not at the top of the array:
|
|
Add together:
|
|
Value of above left square, if not at the left edge of array, else 0
|
|
Value of square above
|
|
Value of above right square, if not at top right edge of array, else 0
|
|
Else: 0
|
|
And then the sum of that plus the same for left and right squares and square below.
|
|
*/
|
|
return (y > 0 ? (x > 0 ? this.state[x-1][y-1] : 0) + this.state[x][y-1] + (x < this.size - 1 ? this.state[x+1][y-1] : 0) : 0) + // Neighbors above
|
|
(x > 0 ? this.state[x-1][y] : 0) + (x < this.size - 1 ? this.state[x+1][y] : 0) + // Neighbors on same row
|
|
(y < this.size - 1 ? (x > 0 ? this.state[x-1][y+1] : 0) + this.state[x][y+1] + (x < this.size - 1 ? this.state[x+1][y+1] : 0) : 0); // Neighbors below
|
|
}
|
|
|
|
/**
|
|
* Check periodic int.
|
|
*
|
|
* @return the int
|
|
*/
|
|
public int checkPeriodic() {
|
|
/*
|
|
Uses the indexOf array-function to search for matches in the list of array-state-hashes, returns > 0 if theres a match.
|
|
If there's a match, compares the arrays using Array-function deepEquals to make sure arrays match as only using
|
|
hashes can lead to false positives.
|
|
*/
|
|
int matches = this.previousBoardsListHash.indexOf(Arrays.deepHashCode(this.state));
|
|
if (matches >= 0 && Arrays.deepEquals(this.previousBoardsList.get(matches), this.state)) {
|
|
this.period = this.iteration - matches;
|
|
this.firstPeriodicState = matches;
|
|
return matches;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Export state to file.
|
|
*
|
|
* @param filename the filename
|
|
* @throws IOException the io exception
|
|
*/
|
|
public void exportStateToFile(String filename) throws IOException {
|
|
/*
|
|
Goes through this.state array and saves it to a file. Example format its saved as:
|
|
0 1 0 0
|
|
1 0 0 0
|
|
1 1 1 1
|
|
0 0 1 1
|
|
*/
|
|
FileWriter fileName = new FileWriter(filename);
|
|
PrintWriter writer = new PrintWriter(fileName);
|
|
for (int i = 0; i < this.state.length; i++) {
|
|
for (int j = 0; j < this.state[i].length; j++) {
|
|
writer.print(this.state[i][j] + (j < this.state[i].length - 1 ? " " : ""));
|
|
}
|
|
writer.println();
|
|
}
|
|
writer.close();
|
|
}
|
|
} |