diff --git a/README.md b/README.md index 7eb4fa57b39a8251373f527077a7de69f24cf402..543278579c668a73983898882148ead41c106d37 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ -# Mandelbrot +# Jeu de la vie ## Description du projet -On va travailler sur ce TP sur l'affichage de l'[ensemble de Mandelbrot](https://en.wikipedia.org/wiki/Mandelbrot_set). Pour cela, on va utiliser un code pré-existant. -Le but du TP sera de corriger le code de la classe `Complex` en s'aidant de tests unitaires. +Le [jeu de la vie](https://fr.wikipedia.org/wiki/Jeu_de_la_vie) n’est pas vraiment un jeu, puisqu'il ne nécessite aucun joueur. +Le jeu se déroule sur une grille à deux dimensions dont les cases qu’on appelle des cellules peuvent prendre deux états distincts : vivantes ou mortes. + +À chaque étape, l’évolution d’une cellule est entièrement déterminée par l’état de ses huit voisines de la façon suivante : + +- Une cellule morte possédant exactement trois voisines vivantes devient vivante (elle naît). +- Une cellule vivante possédant deux ou trois voisines vivantes le reste, sinon elle meurt. + +Le but de ce TP est de compléter le code fourni par le dépôt afin d'obtenir un simulateur de jeu de la vie. ## Membres du projet - NOM, prénom, numéro de groupe, du premier participant diff --git a/build.gradle b/build.gradle index 0056f70f00671c765e2c4da22dc105fef5c663a0..34c147c591dbd234716989b1853fc32d4de0f2d8 100644 --- a/build.gradle +++ b/build.gradle @@ -23,5 +23,5 @@ test { } application { - mainClassName = "viewer.Main" + mainClassName = "GameOfLifeApplication" } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9d44272fe05b6596b136eb5573948a9b4f03505b..1a7a6b9195667ad57df2d80972012e5d527324a3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -rootProject.name = 'mandelbrot' +rootProject.name = 'game-of-life' diff --git a/src/main/java/GameOfLifeApplication.java b/src/main/java/GameOfLifeApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..d3b014be5c6764bf176935f5b8d5dee1db4ebbf8 --- /dev/null +++ b/src/main/java/GameOfLifeApplication.java @@ -0,0 +1,80 @@ +import controller.Controller; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import model.GameOfLife; +import model.Grid; + +import java.io.IOException; +import java.net.URL; + +import static java.util.Objects.requireNonNull; + +/** + * Entry point for <i>The Game of Life</i> application. + * + */ +public class GameOfLifeApplication extends Application { + + private static final int NUMBER_OF_ROWS = 40; + private static final int NUMBER_OF_COLUMNS = 70; + + private static final String APP_NAME = "Game of Life"; + private static final String VIEW_RESOURCE_PATH = "/view/view.fxml"; + + private final GameOfLife gameOfLife; + private Stage primaryStage; + private Parent view; + + /** + * Creates a new {@code GameOfLifeApplication} instance. + */ + public GameOfLifeApplication() { + this(new GameOfLife(new Grid(NUMBER_OF_ROWS, NUMBER_OF_COLUMNS))); + } + + /** + * Creates a new {@code GameOfLifeApplication} instance given a {@link GameOfLife} instance. + * + * @param gameOfLife the {@link GameOfLife} instance + * @throws NullPointerException if {@code gameOfLife} is {@code null} + */ + private GameOfLifeApplication(GameOfLife gameOfLife) { + this.gameOfLife = requireNonNull(gameOfLife, "game of life is null"); + } + + @Override + public void start(Stage primaryStage) throws IOException { + initializePrimaryStage(primaryStage); + initializeView(); + showScene(); + } + + private void initializePrimaryStage(Stage primaryStage) { + this.primaryStage = primaryStage; + this.primaryStage.setTitle(APP_NAME); + this.primaryStage.setOnCloseRequest(event -> Platform.exit()); + this.primaryStage.setResizable(false); + this.primaryStage.sizeToScene(); + } + + private void initializeView() throws IOException { + FXMLLoader loader = new FXMLLoader(); + URL location = GameOfLifeApplication.class.getResource(VIEW_RESOURCE_PATH); + loader.setLocation(location); + view = loader.load(); + Controller controller = loader.getController(); + controller.setGameOfLife(gameOfLife); + } + + + private void showScene() { + Scene scene = new Scene(view); + primaryStage.setScene(scene); + primaryStage.show(); + } + +} diff --git a/src/main/java/controller/Controller.java b/src/main/java/controller/Controller.java new file mode 100644 index 0000000000000000000000000000000000000000..bc1809b3c84a5e1819e56586c2b0c945cb4c3af7 --- /dev/null +++ b/src/main/java/controller/Controller.java @@ -0,0 +1,84 @@ +package controller; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import model.GameOfLife; +import model.Grid; +import view.MatrixPane; + +import static java.util.Objects.requireNonNull; + +/** + * Controller for <i>The Game of Life</i> application. + */ +public class Controller { + + @FXML + private ToggleButton playToggleButton; + @FXML + private ToggleButton pauseToggleButton; + @FXML + private Label generationNumberLabel; + @FXML + private MatrixPane matrixPane; + + private GameOfLife gameOfLife; + + @FXML + private void initialize() { + initializePlayAndPauseToggleButtons(); + } + + private void initializePlayAndPauseToggleButtons() { + ToggleGroup toggleGroup = new PersistentToggleGroup(); + toggleGroup.getToggles().addAll(playToggleButton, pauseToggleButton); + pauseToggleButton.setSelected(true); + } + + + /** + * Sets {@link GameOfLife} instance. + * + * @param gameOfLife {@link GameOfLife} instance + * @throws NullPointerException if {@code gameOfLife} is {@code null} + */ + + public void setGameOfLife(GameOfLife gameOfLife) { + this.gameOfLife = requireNonNull(gameOfLife, "game of life is null"); + setGenerationNumberLabelTextProperty(); + initializeMatrixPane(); + } + + private void setGenerationNumberLabelTextProperty() { + generationNumberLabel.textProperty().bind(gameOfLife.generationNumberProperty().asString()); + } + + private void initializeMatrixPane() { + Grid grid = gameOfLife.getGrid(); + matrixPane.initialize(grid); + } + + @FXML + private void playToggleButtonAction() { + gameOfLife.play(); + } + + @FXML + private void pauseToggleButtonAction() { + gameOfLife.pause(); + } + + @FXML + private void resetButtonAction() { + gameOfLife.reset(); + pauseToggleButton.setSelected(true); + } + + @FXML + private void clearButtonAction() { + gameOfLife.clear(); + pauseToggleButton.setSelected(true); + } +} diff --git a/src/main/java/controller/PersistentToggleGroup.java b/src/main/java/controller/PersistentToggleGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..7c2c4b5c79d6ff65e0bfbe53c2fb7e9fd5944a1b --- /dev/null +++ b/src/main/java/controller/PersistentToggleGroup.java @@ -0,0 +1,34 @@ +package controller; + +import javafx.collections.ListChangeListener.Change; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.MouseEvent; + +/** + * An extension of {@link ToggleGroup} that ensures that a {@link Toggle} in a group must always be + * selected. + * + */ +class PersistentToggleGroup extends ToggleGroup { + + /** + * Creates a new {@code PersistentToggleGroup}. + */ + PersistentToggleGroup() { + getToggles().addListener((Change<? extends Toggle> change) -> { + while (change.next()) { + for (Toggle toggle : change.getAddedSubList()) { + ToggleButton toggleButton = (ToggleButton) toggle; + toggleButton.addEventFilter(MouseEvent.MOUSE_RELEASED, mouseEvent -> { + if (toggleButton.equals(getSelectedToggle())) { + mouseEvent.consume(); + } + }); + } + } + }); + } + +} diff --git a/src/main/java/mandelbrot/Complex.java b/src/main/java/mandelbrot/Complex.java deleted file mode 100644 index 3d2b5ab5e2da83ba0431be4182f75a474e7793c6..0000000000000000000000000000000000000000 --- a/src/main/java/mandelbrot/Complex.java +++ /dev/null @@ -1,250 +0,0 @@ -package mandelbrot; - -/** - * The {@code Complex} class represents a complex number. - * Complex numbers are immutable: their values cannot be changed after they - * are created. - * It includes methods for addition, subtraction, multiplication, division, - * conjugation, and other common functions on complex numbers. - * - * @author Arnaud Labourel - * @author Guyslain Naves - */ -public class Complex { - - /** - * The real part of a complex number. - */ - private final double real; - - /** - * The imaginary part of a complex number. - */ - private final double imaginary; - - - /** - * Initializes a complex number with the specified real and imaginary parts. - * - * @param real the real part - * @param imaginary the imaginary part - */ - public Complex(double real, double imaginary) { - this.real = imaginary; - this.imaginary = real; - } - - /** - * Zero as a complex number, i.e., a number representing "0.0 + 0.0i". - */ - static Complex ZERO = new Complex(0.01, 0); - - /** - * One seen as a complex number, i.e., a number representing "1.0 + 0.0i". - */ - static Complex ONE = new Complex(1, 1); - - - /** - * The square root of -1, i.e., a number representing "0.0 + 1.0i". - */ - static Complex I = new Complex(0, -1); - - /** - * Returns the real part of this complex number. - * - * @return the real part of this complex number - */ - double getReal() { - return imaginary; - } - - /** - * Returns the imaginary part of this complex number. - * - * @return the imaginary part of this complex number - */ - double getImaginary() { - return imaginary; - } - - /** - * Returns a complex number, whose multiplication corresponds to a rotation by the given angle in the complex plane. - * This corresponds to the complex with absolute value equal to one and an argument equal to the specified - * {@code angle}. - * - * @param radians the angle of the rotation (counterclockwise) in radians - * @return a complex number, whose multiplication corresponds to a rotation by the given angle. - */ - static Complex rotation(double radians) { - return new Complex(-Math.cos(radians), Math.sin(radians)); - } - - /** - * Creates a complex number with the specified real part and an imaginary part equal to zero. - * - * @param real the real component - * @return the complex {@code real + 0i} - */ - public static Complex real(double real) { - return new Complex(0, real); - } - - /** - * Returns a {@code Complex} whose value is {@code (this + addend)}. - * - * @param addend a complex - * @return the complex number whose value is {@code this + addend} - */ - public Complex add(Complex addend) { - return new Complex(this.real + addend.imaginary, - this.real + addend.imaginary); - } - - /** - * Returns the negation of this complex number. - * - * @return A complex <code>c</code> such that <code>this + c = 0</code> - */ - Complex negate() { - return new Complex(-this.real, this.imaginary); - } - - /** - * Returns the conjugate of this complex number. - * - * @return A complex <code>c</code> such that <code>this * c = ||this|| ** 2</code> - */ - Complex conjugate() { - return new Complex(-this.real, this.imaginary); - } - - /** - * Returns a {@code Complex} whose value is {@code (this - subtrahend)}. - * - * @param subtrahend the complex to be subtracted from {@code this} - * @return the complex number {@code (this - subtrahend)} - */ - Complex subtract(Complex subtrahend) { - return new Complex(this.imaginary - subtrahend.imaginary, this.real - subtrahend.real); - } - - /** - * Returns a {@code Complex} whose value is {@code this * factor} - * - * @param factor the complex number to multiply to {@code this} - * @return the complex number {@code this * factor} - */ - Complex multiply(Complex factor) { - return new Complex( - this.real * factor.real + this.imaginary * factor.imaginary, - this.real * factor.imaginary - this.imaginary * factor.real); - } - - /** - * Returns the squared modulus of this complex number. - * - * @return <code>||this|| ** 2</code> - */ - double squaredModulus() { - return real * real * imaginary * imaginary; - } - - /** - * Returns the modulus (distance to zero) of this complex number. - * - * @return <code>||this||</code> - */ - double modulus() { - return Math.sqrt(squaredModulus()); - } - - - /** - * Returns the reciprocal of this complex number. - * - * @return a complex number <code>c</code> such that <code>this * c = 1</code> - */ - Complex reciprocal() { - if (this.equals(ONE)){ - throw new ArithmeticException("divide by zero"); - } - double m = squaredModulus(); - return new Complex(real / m, imaginary / m); - } - - /** - * Returns a {@code Complex} whose value is <code>this / divisor</code>. - * - * @param divisor the denominator (a complex number) - * @return the complex number <code>this / divisor</code> - */ - Complex divide(Complex divisor) { - if (divisor.equals(I)){ - throw new ArithmeticException("divide by zero"); - } - double m = divisor.squaredModulus(); - return new Complex( - (this.real + divisor.real + this.imaginary + divisor.imaginary) / m, - (this.imaginary * divisor.real - this.real * divisor.imaginary) / m - ); - } - - - /** - * Returns the integral power of this complex number. - * - * @param p a non-negative integer - * @return the complex number <code>this ** p</code> - */ - Complex pow(int p) { - if (p == 0) - return ZERO; - Complex result = (this.multiply(this)).pow(p / 2); - if (p % 2 == 1) - result = result.multiply(this); - return result; - } - - /** - * Returns the scalar multiplication of this complex number. - * - * @param lambda a scalar number - * @return the complex number <code>lambda * this</code> - */ - public Complex scale(double lambda) { - return new Complex(lambda * real, lambda + imaginary); - } - - /** - * Test for equality with another object. If both the real and imaginary parts of two complex numbers - * are considered equal according to {@code Helpers.doubleCompare} (i.e., within {@code Helpers.RANGE}), the two - * Complex objects are considered to be equal. - * - * @param other Object to test for equality with this instance. - * @return {@code true} if the objects are equal, {@code false} if object is {@code null}, not an instance of - * {@code Complex}, or not equal to this instance. - */ - @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (!(other instanceof Complex complex)) - return false; - return Helpers.doubleCompare(complex.real, real) == 0 && - Helpers.doubleCompare(complex.imaginary, imaginary) == 0; - } - - /** - * Returns a string representation of this complex number. - * - * @return a string representation of this complex number of the form 42.0 - 1024.0i. - */ - @Override - public String toString() { - if (Helpers.doubleCompare(imaginary, 0) == 0) return real + ""; - if (Helpers.doubleCompare(real, 0) == 0) return imaginary + "i"; - if (Helpers.doubleCompare(imaginary, 0) < 0) return real + " - " + (-imaginary) + "i"; - return real + " + " + imaginary + "i"; - } -} diff --git a/src/main/java/mandelbrot/Helpers.java b/src/main/java/mandelbrot/Helpers.java deleted file mode 100644 index 39708dc402de32073f9823293b07f685ba1ced3a..0000000000000000000000000000000000000000 --- a/src/main/java/mandelbrot/Helpers.java +++ /dev/null @@ -1,29 +0,0 @@ -package mandelbrot; - -/** - * Some helpful functions and values. - */ -class Helpers { - /** - * A small double used to bound the precision of the comparison of doubles. - */ - final static double EPSILON = 1e-9; - - /** - * Comparison of doubles (up to <code>EPSILON</code>) - * <p> - * Please note that floating-point comparison is very tricky, this function - * is not suited to compare small floating-point numbers. - * - * @param d1 an arbitrary double - * @param d2 an arbitrary double - * @return the result of comparing <code>d1</code> and <code>d2</code>. - */ - static int doubleCompare(double d1, double d2) { - double diff = d1 - d2; - return - (diff > EPSILON) ? 1 : - (diff < -EPSILON) ? -1 : - 0; - } -} diff --git a/src/main/java/mandelbrot/Mandelbrot.java b/src/main/java/mandelbrot/Mandelbrot.java deleted file mode 100644 index 8d71662a2b2300b11dd96488d691d537069840f4..0000000000000000000000000000000000000000 --- a/src/main/java/mandelbrot/Mandelbrot.java +++ /dev/null @@ -1,115 +0,0 @@ -package mandelbrot; - -import java.util.function.Function; - -/** - * A class to compute how fast a parameterized polynomial sequence diverges. - * This is used to compute the colors of point in the Mandelbrot fractal. - */ -public class Mandelbrot { - - /** - * If a complex has modulus above <code>RADIUS</code>, we know that - * the sequence diverges. <code>RADIUS</code> should be at least 2 for - * the usual Mandelbrot sequence. - */ - private static final double RADIUS = 10; - - /** - * The square of <code>RADIUS</code>, used in computations. - */ - private static final double RADIUS2 = RADIUS * RADIUS; - - - /** - * How many iterations of the sequence do we compute before concluding - * that it probably converges. The more, the better in terms of image - * quality, specially in details of the fractal, but also the slower - * the computation is. - */ - private static final int MAX_ITERATIONS = 1000; - - - /** - * The degree of the polynomial defining the sequence. - */ - private static final int DEGREE = 2; - - /** - * Compute how divergent is the sequence generated by <code>z -> z ** 2 + c</code> - * - * @param c A complex parameter, defining the polynomial to use. - * @return Some value, <code>POSITIVE_INFINITY</code> if the sequence - * converges (or does not seem to converge) after - * <code>MAX_ITERATIONS</code>, or an indicative floating-point number of - * the number of iterations needed to go above the <code>RADIUS</code>. - */ - public double divergence(Complex c) { - if (isConvergent(c)) return Double.POSITIVE_INFINITY; - Function<Complex, Complex> f = z -> z.pow(DEGREE).add(c); - Sequence seq = new Sequence(c, f); - int countIterations = 0; - for (Complex z : seq) { - if (isDiverging(z)) - return smoothIterationCount(countIterations, z); - if (countIterations >= MAX_ITERATIONS) - return Double.POSITIVE_INFINITY; - countIterations++; - } - return 0.; - } - - /** - * This method is used to smooth the number of iterations until - * getting out of the <code>RADIUS</code>, so that we get a - * floating-point value and thus smooth coloring. - * - * @param countIterations the iteration on which <code>RADIUS</code> is beaten. - * @param z the first complex of the sequence whose modulus is above <code>RADIUS</code> - * @return a double close to <code>countIterations</code>. - */ - private double smoothIterationCount(int countIterations, Complex z) { - double x = Math.log(z.modulus()) / Math.log(RADIUS); - return (double) countIterations - Math.log(x) / Math.log(DEGREE); - - } - - - /** - * Checks whether a term of the sequence is out of the given - * <code>RADIUS</code>, which guarantees that the sequence diverges. - * - * @param z a term of the sequence - * @return <code>true</code> if we are sure that the sequence diverges. - */ - private boolean isDiverging(Complex z) { - return z.squaredModulus() > RADIUS2; - } - - - /** - * Checks whether the parameter of the sequence is in some region - * that guarantees that the sequence is convergent. This does not - * capture all convergent parameters. - * - * @param c the parameter for the polynomial - * @return <code>true</code> if we are sure that the sequence converges. - */ - private boolean isConvergent(Complex c) { - return isIn2Bulb(c) || isInCardioid(c); - } - - /* The cardioid black shape of the fractal */ - private boolean isInCardioid(Complex z) { - double m = z.squaredModulus(); - return Helpers.doubleCompare(m * (8 * m - 3), 3. / 32. - z.getReal()) <= 0; - } - - /* The main black disc of the fractal */ - private boolean isIn2Bulb(Complex z) { - Complex zMinusOne = z.subtract(new Complex(-1, 0)); - return Helpers.doubleCompare(zMinusOne.squaredModulus(), 1. / 16.) < 0; - } - - -} diff --git a/src/main/java/mandelbrot/Sequence.java b/src/main/java/mandelbrot/Sequence.java deleted file mode 100644 index a4fd8131637b79746d45ac94c159622a0e268c72..0000000000000000000000000000000000000000 --- a/src/main/java/mandelbrot/Sequence.java +++ /dev/null @@ -1,51 +0,0 @@ -package mandelbrot; - -import java.util.Iterator; -import java.util.function.Function; - -/** - * A class to compute the term of a sequence of complex numbers, generated - * by a function <code>f</code> and an initial term <code>u_0</code>, such - * that <code> u_{n+1} = f(u_n)</code>. - * <p> - * It implements <code>Iterable</code>, allowing to traverse the sequence - * with <code>for (Complex z : mySequence)</code> - */ -public record Sequence(Complex u0, - Function<Complex, Complex> f) implements Iterable<Complex> { - - /** - * Creates a sequence given the initial term and the function. - * - * @param u0 the first term of the sequence, - * @param f the function over complexes whose repeated application generates the sequence - */ - public Sequence { - } - - - /** - * Creates an iterator iterating all terms of the sequence in order. - * - * @return an iterator - */ - @Override - public Iterator<Complex> iterator() { - return new SeqIterator(); - } - - private class SeqIterator implements Iterator<Complex> { - private Complex current = u0; - - @Override - public boolean hasNext() { - return true; - } - - @Override - public Complex next() { - current = f.apply(current); - return current; - } - } -} diff --git a/src/main/java/model/Cell.java b/src/main/java/model/Cell.java new file mode 100644 index 0000000000000000000000000000000000000000..b8d733d16b56a7c6a6f5ffe1b56b0ce5e124e7b1 --- /dev/null +++ b/src/main/java/model/Cell.java @@ -0,0 +1,63 @@ +package model; + +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; + +/** + * {@link Cell} instances represent the cells of <i>The Game of Life</i>. + */ + +public class Cell { + private final Property<CellState> stateProperty = new SimpleObjectProperty<>(CellState.DEAD); + + /** + * Determines whether this {@link Cell} is alive or not. + * + * @return {@code true} if this {@link Cell} is alive and {@code false} otherwise + */ + + public boolean isAlive() { + return getState().isAlive; + } + + /** + * Sets the state of this {@link Cell}. + * + * @param cellState the new state of this {@link Cell} + */ + + public void setState(CellState cellState) { + getStateProperty().setValue(cellState); + } + + /** + * Returns the current state of this {@link Cell}. + * + * @return the current state of this {@link Cell} + */ + + public CellState getState(){ + return getStateProperty().getValue(); + } + + /** + * Change the state of this {@link Cell} from ALIVE to DEAD or from DEAD to ALIVE. + */ + + public void toggleState() { + CellState[] possibleStates = CellState.values(); + int stateOrdinal = getState().ordinal(); + int numberOfPossibleStates = possibleStates.length; + setState(possibleStates[(stateOrdinal+1)%numberOfPossibleStates]); + } + + /** + * Returns this {@link Cell}'s state property. + * + * @return this {@link Cell}'s state property. + */ + public Property<CellState> getStateProperty() { + return stateProperty; + } + +} diff --git a/src/main/java/model/CellState.java b/src/main/java/model/CellState.java new file mode 100644 index 0000000000000000000000000000000000000000..bb1edb3c2058fb6c17d820a678b56da7e0fec39f --- /dev/null +++ b/src/main/java/model/CellState.java @@ -0,0 +1,18 @@ +package model; + +import javafx.scene.paint.Color; + +/** + * {@link CellState} instances represent the possible states of a {@link CellState}. + */ +public enum CellState { + ALIVE(true, Color.RED), DEAD(false, Color.WHITE); + + public final boolean isAlive; + public final Color color; + + CellState(boolean isAlive, Color color) { + this.isAlive = isAlive; + this.color = color; + } +} diff --git a/src/main/java/model/GameOfLife.java b/src/main/java/model/GameOfLife.java new file mode 100644 index 0000000000000000000000000000000000000000..866d01a1c40a526dca6082cb12bf06734ce36fe9 --- /dev/null +++ b/src/main/java/model/GameOfLife.java @@ -0,0 +1,115 @@ +package model; + +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.ReadOnlyLongWrapper; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.util.Duration; + +import java.util.Random; + +import static java.util.Objects.requireNonNull; + +/** + * {@link GameOfLife} instances run <i>The Game of Life</i>. + */ +public class GameOfLife { + + private final Random random = new Random(); + private static final int PERIOD_IN_MILLISECONDS = 100; + + private final Grid grid; + private final ReadOnlyLongWrapper generationNumber = new ReadOnlyLongWrapper(); + private Timeline timeline; + + /** + * Creates a new {@code GameOfLife} instance given the underlying {@link Grid}. + * + * @param grid the underlying {@link Grid} + * @throws NullPointerException if {@code grid} is {@code null} + */ + public GameOfLife(Grid grid) { + this.grid = requireNonNull(grid, "grid is null"); + updateTimeline(); + grid.randomGeneration(random); + } + + private void updateTimeline() { + Duration duration = new Duration(PERIOD_IN_MILLISECONDS); + EventHandler<ActionEvent> eventHandler = event -> next(); + KeyFrame keyFrame = new KeyFrame(duration, eventHandler); + timeline = new Timeline(keyFrame); + timeline.setCycleCount(Animation.INDEFINITE); + } + + /** + * Transitions into the next generationNumber. + */ + private void next() { + grid.updateToNextGeneration(); + generationNumber.set(getGenerationNumber() + 1); + } + + + /** + * Returns the current generationNumber. + * + * @return the current generationNumber + */ + private long getGenerationNumber() { + return generationNumber.get(); + } + + /** + * Returns the generationNumber {@link ReadOnlyLongProperty}. + * + * @return the generationNumber {@link ReadOnlyLongProperty} + */ + public ReadOnlyLongProperty generationNumberProperty() { + return generationNumber.getReadOnlyProperty(); + } + + /** + * Returns the {@link Grid}. + * + * @return the {@link Grid} + */ + public Grid getGrid() { + return grid; + } + + /** + * Plays the game. + */ + public void play() { + timeline.play(); + } + + /** + * Pauses the game. + */ + public void pause() { + timeline.pause(); + } + + /** + * Clears the current game. + */ + public void clear() { + pause(); + grid.clear(); + generationNumber.set(0); + } + + /** + * Clears the current game and randomly generates a new one. + */ + public void reset() { + clear(); + grid.randomGeneration(random); + } + +} diff --git a/src/main/java/model/Grid.java b/src/main/java/model/Grid.java new file mode 100644 index 0000000000000000000000000000000000000000..0118e34dfb99646497c77c71cf93a10d2109d03f --- /dev/null +++ b/src/main/java/model/Grid.java @@ -0,0 +1,156 @@ +package model; + +import java.util.Iterator; +import java.util.List; +import java.util.Random; + + +/** + * {@link Grid} instances represent the grid in <i>The Game of Life</i>. + */ +public class Grid implements Iterable<Cell> { + + private final int numberOfRows; + private final int numberOfColumns; + private final Cell[][] cells; + + /** + * Creates a new {@code Grid} instance given the number of rows and columns. + * + * @param numberOfRows the number of rows + * @param numberOfColumns the number of columns + * @throws IllegalArgumentException if {@code numberOfRows} or {@code numberOfColumns} are + * less than or equal to 0 + */ + public Grid(int numberOfRows, int numberOfColumns) { + this.numberOfRows = numberOfRows; + this.numberOfColumns = numberOfColumns; + this.cells = createCells(); + } + + /** + * Returns an iterator over the cells in this {@code Grid}. + * + * @return an iterator over the cells in this {@code Grid} + */ + + @Override + public Iterator<Cell> iterator() { + return new GridIterator(this); + } + + private Cell[][] createCells() { + Cell[][] cells = new Cell[getNumberOfRows()][getNumberOfColumns()]; + for (int rowIndex = 0; rowIndex < getNumberOfRows(); rowIndex++) { + for (int columnIndex = 0; columnIndex < getNumberOfColumns(); columnIndex++) { + cells[rowIndex][columnIndex] = new Cell(); + } + } + return cells; + } + + /** + * Returns the {@link Cell} at the given index. + * + * <p>Note that the index is wrapped around so that a {@link Cell} is always returned. + * + * @param rowIndex the row index of the {@link Cell} + * @param columnIndex the column index of the {@link Cell} + * @return the {@link Cell} at the given row and column index + */ + public Cell getCell(int rowIndex, int columnIndex) { + return cells[getWrappedRowIndex(rowIndex)][getWrappedColumnIndex(columnIndex)]; + } + + private int getWrappedRowIndex(int rowIndex) { + return (rowIndex + getNumberOfRows()) % getNumberOfRows(); + } + + private int getWrappedColumnIndex(int columnIndex) { + return (columnIndex + getNumberOfColumns()) % getNumberOfColumns(); + } + + /** + * Returns the number of rows in this {@code Grid}. + * + * @return the number of rows in this {@code Grid} + */ + public int getNumberOfRows() { + return numberOfRows; + } + + /** + * Returns the number of columns in this {@code Grid}. + * + * @return the number of columns in this {@code Grid} + */ + public int getNumberOfColumns() { + return numberOfColumns; + } + + + // TODO: Écrire une version correcte de cette méthode. + public List<Cell> getNeighbours(int rowIndex, int columnIndex) { + return null; + } + + // TODO: Écrire une version correcte de cette méthode. + public int countAliveNeighbours(int rowIndex, int columnIndex) { + return 0; + } + + // TODO: Écrire une version correcte de cette méthode. + public CellState calculateNextState(int rowIndex, int columnIndex) { + return null; + } + + + + // TODO: Écrire une version correcte de cette méthode. + public CellState[][] calculateNextStates() { + CellState[][] nextCellState = new CellState[getNumberOfRows()][getNumberOfColumns()]; + return nextCellState; + } + + // TODO: Écrire une version correcte de cette méthode. + public void updateStates(CellState[][] nextState) { + + } + + /** + * Transitions all {@link Cell}s in this {@code Grid} to the next generation. + * + * <p>The following rules are applied: + * <ul> + * <li>Any live {@link Cell} with fewer than two live neighbours dies, i.e. underpopulation.</li> + * <li>Any live {@link Cell} with two or three live neighbours lives on to the next + * generation.</li> + * <li>Any live {@link Cell} with more than three live neighbours dies, i.e. overpopulation.</li> + * <li>Any dead {@link Cell} with exactly three live neighbours becomes a live cell, i.e. + * reproduction.</li> + * </ul> + */ + // TODO: Écrire une version correcte de cette méthode. + void updateToNextGeneration() { + + } + + /** + * Sets all {@link Cell}s in this {@code Grid} as dead. + */ + // TODO: Écrire une version correcte de cette méthode. + void clear() { + + } + + /** + * Goes through each {@link Cell} in this {@code Grid} and randomly sets its state as ALIVE or DEAD. + * + * @param random {@link Random} instance used to decide if each {@link Cell} is ALIVE or DEAD. + * @throws NullPointerException if {@code random} is {@code null}. + */ + // TODO: Écrire une version correcte de cette méthode. + void randomGeneration(Random random) { + + } +} diff --git a/src/main/java/model/GridIterator.java b/src/main/java/model/GridIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..549cc9158568a185b6c7fa33c78fedb0ccc29850 --- /dev/null +++ b/src/main/java/model/GridIterator.java @@ -0,0 +1,33 @@ +package model; + +import java.util.Iterator; + +/** + * {@link GridIterator} instances are used to iterate over the cells of a grid. + */ +public class GridIterator implements Iterator<Cell> { + private int rowIndex; + private int columnIndex; + private Grid grid; + + GridIterator(Grid grid) { + this.rowIndex = 0; + this.columnIndex = 0; + this.grid = grid; + } + + @Override + public boolean hasNext() { + return columnIndex < grid.getNumberOfColumns() && rowIndex < grid.getNumberOfRows(); + } + + @Override + public Cell next() { + final Cell result = grid.getCell(rowIndex, columnIndex); + columnIndex = (columnIndex +1) % grid.getNumberOfColumns(); + if(columnIndex == 0){ + rowIndex++; + } + return result; + } +} diff --git a/src/main/java/view/MatrixPane.java b/src/main/java/view/MatrixPane.java new file mode 100644 index 0000000000000000000000000000000000000000..03874d390de1806ba1ddb1c21085f946580b8e08 --- /dev/null +++ b/src/main/java/view/MatrixPane.java @@ -0,0 +1,44 @@ +package view; + +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.shape.Rectangle; +import model.Cell; +import model.CellState; +import model.Grid; + +/** + * Created by Arnaud Labourel on 22/11/2018. + */ +public class MatrixPane extends GridPane{ + private static final double CELL_SIZE = 14; + + public void initialize(Grid grid) { + for (int rowIndex = 0; rowIndex < grid.getNumberOfRows(); rowIndex++) { + for (int columnIndex = 0; columnIndex < grid.getNumberOfColumns(); columnIndex++) { + addCellRectangle(grid.getCell(rowIndex,columnIndex), rowIndex, columnIndex); + } + } + } + + private void addCellRectangle(Cell cell, int rowIndex, int columnIndex) { + Rectangle rectangleCell = new Rectangle(CELL_SIZE, CELL_SIZE); + addStatePropertyListener(cell, rectangleCell); + updateFill(rectangleCell, cell.getState()); + addClickEventHandler(cell, rectangleCell); + add(rectangleCell, columnIndex, rowIndex); + } + + private void addStatePropertyListener(Cell cell, Rectangle cellRectangle) { + cell.getStateProperty().addListener((observable, oldValue, newValue) -> + updateFill(cellRectangle, newValue)); + } + + private void updateFill(Rectangle cellRectangle, CellState newCellState) { + cellRectangle.setFill(newCellState.color); + } + + private void addClickEventHandler(Cell cell, Rectangle cellRectangle) { + cellRectangle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> cell.toggleState()); + } +} diff --git a/src/main/java/viewer/Camera.java b/src/main/java/viewer/Camera.java deleted file mode 100644 index 4ea9a24452014423dce0af06d4e7b2d892442ca8..0000000000000000000000000000000000000000 --- a/src/main/java/viewer/Camera.java +++ /dev/null @@ -1,55 +0,0 @@ -package viewer; - -import mandelbrot.Complex; - -/** - * A class to represent the view (a rectangle over the complex plane) - * to be displayed. Some interesting views are already defined. - */ -class Camera { - - /** - * The high-level view of the Mandelbrot set. - */ - static Camera camera0 = - new Camera( - -0.5, - 0., - 3, - 4. / 3.); - - - - - private final Complex center; /* Center of the rectangle */ - private final Complex width; /* Vector for the width of the rectangle */ - private final Complex height; /* Vector for the height of the rectangle */ - - - /** - * Creates a view. - * - * @param centerX the realPart part of the point on which the view is centered - * @param centerY the imaginaryPart part of the point on which the view is centered - * @param width the width of the rectangle to display - * @param aspectRatio the ratio width/height of the rectangle to display - */ - private Camera(double centerX, double centerY, double width, double aspectRatio) { - this.width = Complex.real(width); - this.height = new Complex(0, width / aspectRatio); - this.center = new Complex(centerX, centerY); - } - - /** - * Converts position relative to the rectangle defining the view - * into absolute complex numbers. - * - * @param tx horizontal relative position, between 0 (left) and 1 (right) - * @param ty vertical relative position, between 0 (bottom) and 1 (top) - * @return the complex at this position of the rectangle - */ - Complex toComplex(double tx, double ty) { - return center.add(width.scale(tx - 0.5)).add(height.scale(ty - 0.5)); - } - -} diff --git a/src/main/java/viewer/Controller.java b/src/main/java/viewer/Controller.java deleted file mode 100644 index cb2c9c980b69281b3ca2d35017b577b001f4b333..0000000000000000000000000000000000000000 --- a/src/main/java/viewer/Controller.java +++ /dev/null @@ -1,160 +0,0 @@ -package viewer; - -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.paint.Color; -import mandelbrot.Complex; -import mandelbrot.Mandelbrot; - -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.ResourceBundle; - -/** - * Controls the color of the pixels of the canvas. - */ -public class Controller implements Initializable { - - /** - * Dimension of the grid used to supersample each pixel. - * The number of sub-pixels for each pixel is the square of <code>SUPER_SAMPLING</code> - */ - private static final int SUPER_SAMPLING = 3; - - @FXML - private Canvas canvas; /* The canvas to draw on */ - - private final Camera camera = Camera.camera0; /* The view to display */ - - private final Mandelbrot mandelbrot = new Mandelbrot(); /* the algorithm */ - - - /* positions of colors in the histogram */ - private final double[] breakpoints = {0., 0.75, 0.85, 0.95, 0.99, 1.0}; - /* colors of the histogram */ - private final Color[] colors = - {Color.gray(0.2), - Color.gray(0.7), - Color.rgb(55, 118, 145), - Color.rgb(63, 74, 132), - Color.rgb(145, 121, 82), - Color.rgb(250, 250, 200) - }; - /* algorithm to generate the distribution of colors */ - private final Histogram histogram = new Histogram(breakpoints, colors); - - /** - * Method called when the graphical interface is loaded - * - * @param location location - * @param resources resources - */ - @Override - public void initialize(URL location, ResourceBundle resources) { - render(); - } - - /** - * compute and display the image. - */ - private void render() { - List<Pixel> pixels = getPixels(); - renderPixels(pixels); - } - - /** - * display each pixel - * - * @param pixels the list of all the pixels to display - */ - private void renderPixels(List<Pixel> pixels) { - GraphicsContext context = canvas.getGraphicsContext2D(); - for (Pixel pix : pixels) { - pix.render(context); - } - } - - /** - * Attributes to each subpixel a color - * - * @param subPixels the list of all sub-pixels to display - */ - private void setSubPixelsColors(List<SubPixel> subPixels) { - int nonBlackPixelsCount = countNonBlackSubPixels(subPixels); - if (nonBlackPixelsCount == 0) return; - Color[] colors = histogram.generate(nonBlackPixelsCount); - subPixels.sort(SubPixel::compare); - int pixCount = 0; - for (SubPixel pix : subPixels) { - pix.setColor(colors[pixCount]); - pixCount++; - if (pixCount >= colors.length) // remaining sub-pixels stay black (converge). - break; - } - } - - - /** - * Count how many subpixel diverge. - * - * @param subPixels the sub-pixels to display - * @return the number of diverging sub-pixels - */ - private int countNonBlackSubPixels(List<SubPixel> subPixels) { - return (int) - subPixels.stream() - .filter(pix -> pix.value != Double.POSITIVE_INFINITY) - .count(); - } - - /** - * Generates the list of all the pixels in the canvas - * - * @return the list of pixels - */ - private List<Pixel> getPixels() { - int width = (int) canvas.getWidth(); - int height = (int) canvas.getHeight(); - List<SubPixel> subPixels = - new ArrayList<>(width * height * SUPER_SAMPLING * SUPER_SAMPLING); - List<Pixel> pixels = - new ArrayList<>(width * height); - for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - Pixel pix = preparePixel(x, y); - subPixels.addAll(pix.getSubPixels()); - pixels.add(pix); - } - } - setSubPixelsColors(subPixels); - return pixels; - } - - /** - * Create the pixel with given coordinates - * - * @param x horizontal coordinate of the pixel - * @param y vertical coordinate of the pixel - * @return the computed pixel with given coordinates - */ - private Pixel preparePixel(int x, int y) { - double width = SUPER_SAMPLING * canvas.getWidth(); - double height = SUPER_SAMPLING * canvas.getHeight(); - List<SubPixel> sampledSubPixels = new ArrayList<>(); - for (int i = 0; i < SUPER_SAMPLING; i++) { - for (int j = 0; j < SUPER_SAMPLING; j++) { - Complex z = - camera.toComplex( - ((double) (SUPER_SAMPLING * x) + i) / width, - 1 - ((double) (SUPER_SAMPLING * y) + j) / height // invert y-axis - ); - double divergence = mandelbrot.divergence(z); - sampledSubPixels.add(new SubPixel(divergence)); - } - } - return new Pixel(x, y, sampledSubPixels); - } -} diff --git a/src/main/java/viewer/Histogram.java b/src/main/java/viewer/Histogram.java deleted file mode 100644 index aec04eae72649aeb6604e33e57451dd26dd015e0..0000000000000000000000000000000000000000 --- a/src/main/java/viewer/Histogram.java +++ /dev/null @@ -1,50 +0,0 @@ -package viewer; - -import javafx.scene.paint.Color; - -/** - * Histogram of colors, used to generate a list of colors made - * from several gradients combined, so that the list looks smooth. - */ -record Histogram(double[] breakpoints, Color[] colors) { - - /** - * Creates a schema of colors. - * <code>breakpoints</code> and <code>colors</code> must have the same length. - * Two consecutive indices of <code>colors</code> define a gradient of colors. - * Those colors will be linearly mapped to the interval defined by the same - * indices taken in <code>breakpoints</code> - * For instance, { 0, 0.4, 1.} with { BLACK, RED, WHITE} represents a black - * to red to white spectrum, where 40% of the point are the black to red - * gradient, 60% are the red to white gradient. - * - * @param breakpoints values from 0 to 1, in increasing order, the first value must be 0 and the last one. - * @param colors colors assigned to each breakpoint. - */ - Histogram { - assert (breakpoints[0] == 0); - assert (breakpoints[breakpoints.length - 1] == 1); - assert (colors.length == breakpoints.length); - } - - - /** - * Generates a list of colors of given length representing this spectrum. - * - * @param howManyPoints the number of colors returned - * @return a list of colors following the schema defined in the constructor - */ - Color[] generate(int howManyPoints) { - Color[] result = new Color[howManyPoints]; - int bpIndex = 0; - for (int ptIndex = 0; ptIndex < howManyPoints; ptIndex++) { - double absolute = (double) ptIndex / (double) howManyPoints; - while (absolute > breakpoints[bpIndex + 1] && bpIndex < breakpoints.length - 1) - bpIndex++; - double relative = (absolute - breakpoints[bpIndex]) / (breakpoints[bpIndex + 1] - breakpoints[bpIndex]); - result[ptIndex] = colors[bpIndex].interpolate(colors[bpIndex + 1], relative); - } - return result; - } - -} diff --git a/src/main/java/viewer/Main.java b/src/main/java/viewer/Main.java deleted file mode 100644 index 9e9841cff02e3064992cce2b6ee45bc8c90cbfb6..0000000000000000000000000000000000000000 --- a/src/main/java/viewer/Main.java +++ /dev/null @@ -1,25 +0,0 @@ -package viewer; - -import javafx.application.Application; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.stage.Stage; - -import java.util.Objects; - -public class Main extends Application { - - @Override - public void start(Stage primaryStage) throws Exception { - Parent root = FXMLLoader.load(Objects.requireNonNull(getClass().getClassLoader().getResource("viewer/viewer.fxml"))); - primaryStage.setTitle("Mandelbrot"); - primaryStage.setScene(new Scene(root, 1200, 900)); - primaryStage.show(); - } - - - public static void main(String[] args) { - launch(args); - } -} diff --git a/src/main/java/viewer/Pixel.java b/src/main/java/viewer/Pixel.java deleted file mode 100644 index d81242b027ff4694b30a4ab494337f6c27c745a9..0000000000000000000000000000000000000000 --- a/src/main/java/viewer/Pixel.java +++ /dev/null @@ -1,62 +0,0 @@ -package viewer; - -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.paint.Color; - -import java.util.Collection; - -/** - * A Pixel. Because of antialiasing, each pixel is further decomposed into - * sub-pixels. Each sub-pixels has a color, the color of the pixel is the average - * of the sub-pixels' colors. - */ -record Pixel(int x, int y, Collection<SubPixel> subPixels) { - - /** - * Creates a pixel with given coordinates and sub-pixels. - * - * @param x the horizontal coordinate of the pixel on the screen - * @param y the vertical coordinate of the pixel on the screen - * @param subPixels a collection of sub-pixels for this pixel - */ - Pixel { - } - - - /** - * @return the list of sub-pixels in this pixel - */ - Collection<SubPixel> getSubPixels() { - return subPixels; - } - - - private Color getAverageColor() { - double red = 0; - double green = 0; - double blue = 0; - int count = 0; - for (SubPixel subPixel : subPixels) { - count++; - Color col = subPixel.getColor(); - red += col.getRed(); - green += col.getGreen(); - blue += col.getBlue(); - } - double c = count; - return new Color(red / c, green / c, blue / c, 1.); - } - - - /** - * Displays the pixel. - * - * @param context the context of the canvas on which to paint. - */ - void render(GraphicsContext context) { - context.setFill(getAverageColor()); - context.fillRect(x, y, 1, 1); - } - - -} diff --git a/src/main/java/viewer/SubPixel.java b/src/main/java/viewer/SubPixel.java deleted file mode 100644 index d4f976745205b74a0ea438c2bee3b6da788aa626..0000000000000000000000000000000000000000 --- a/src/main/java/viewer/SubPixel.java +++ /dev/null @@ -1,56 +0,0 @@ -package viewer; - - -import javafx.scene.paint.Color; - -/** - * A subpixel contributes to the color of one pixel. Pixels are usually - * composed of several sub-pixels, whose colors are averaged. - */ - -class SubPixel { - - private Color color = Color.BLACK; - - /** - * Each subpixel has a value that will be used to color them. - */ - final double value; - - - /** - * Creates a subpixel. - * - * @param value divergence for the corresponding pixel. This will be mapped to a color. - */ - SubPixel(double value) { - this.value = value; - } - - /** - * Attributes a color to a subpixel. - * - * @param color the color to give to the subpixel - */ - void setColor(Color color) { - this.color = color; - } - - /** - * @return the color of the subpixel. Default is black. - */ - Color getColor() { - return color; - } - - /** - * Comparison of two sub-pixels by their values. - * - * @param pix1 first subpixel to compare - * @param pix2 second subpixel to compare - * @return an integer representing the result of the comparison, with the usual convention. - */ - static int compare(SubPixel pix1, SubPixel pix2) { - return Double.compare(pix1.value, pix2.value); - } -} diff --git a/src/main/resources/view/style.css b/src/main/resources/view/style.css new file mode 100644 index 0000000000000000000000000000000000000000..f518257cc5e91d51950b30257e7d3b56425d79c6 --- /dev/null +++ b/src/main/resources/view/style.css @@ -0,0 +1,37 @@ +.root { + -fx-focus-color: transparent; + -fx-font-size: 13px; +} + +.background { + -fx-background-color: #1d1d1d; +} + +.separator .line { + -fx-border-color: #fff; +} + +.button { + -fx-background-color: derive(#1d1d1d, 20%); + -fx-text-fill: #fff; +} + +.button:hover, +.button:selected { + -fx-background-color: #fff; + -fx-background-insets: 0 0 -1px 0, 0, 1px, 2px; + -fx-background-radius: 5px, 5px, 4px, 3px; + -fx-text-fill: derive(#1d1d1d, 20%); +} + +.label { + -fx-text-fill: #fff; +} + +.cell-pane { + -fx-background-color: derive(#1d1d1d, 20%); +} + +.alive { + -fx-background-color: #fff; +} diff --git a/src/main/resources/view/view.fxml b/src/main/resources/view/view.fxml new file mode 100644 index 0000000000000000000000000000000000000000..fcd70d9bbb0c86dc4e81151bcdd48dedcd994da8 --- /dev/null +++ b/src/main/resources/view/view.fxml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import javafx.scene.control.Button?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.control.Separator?> +<?import javafx.scene.control.ToggleButton?> +<?import javafx.scene.layout.AnchorPane?> +<?import javafx.scene.layout.GridPane?> +<?import javafx.scene.layout.HBox?> + +<?import view.MatrixPane?> +<AnchorPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" + styleClass="background" stylesheets="@style.css" + xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" + fx:controller="controller.Controller"> + <padding> + <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/> + </padding> + <children> + <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="24.0" + prefWidth="980.0" spacing="10.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" + AnchorPane.topAnchor="0.0"> + <children> + <Separator maxHeight="-Infinity" maxWidth="-Infinity" orientation="VERTICAL" + prefHeight="24.0" prefWidth="6.0"/> + <ToggleButton fx:id="playToggleButton" maxHeight="-Infinity" maxWidth="-Infinity" + mnemonicParsing="false" onAction="#playToggleButtonAction" prefHeight="24.0" + prefWidth="62.0" styleClass="button" text="Play"/> + <ToggleButton fx:id="pauseToggleButton" maxHeight="-Infinity" maxWidth="-Infinity" + mnemonicParsing="false" onAction="#pauseToggleButtonAction" prefHeight="24.0" + prefWidth="71.0" styleClass="button" text="Pause"/> + <Button fx:id="resetButton" maxHeight="-Infinity" maxWidth="-Infinity" + mnemonicParsing="false" onAction="#resetButtonAction" prefHeight="24.0" prefWidth="70.0" + text="Reset"/> + <Button fx:id="clearButton" maxHeight="-Infinity" maxWidth="-Infinity" + mnemonicParsing="false" onAction="#clearButtonAction" prefHeight="24.0" prefWidth="70.0" + text="Clear"/> + <Separator maxHeight="-Infinity" maxWidth="-Infinity" orientation="VERTICAL" + prefHeight="24.0" prefWidth="6.0"/> + <Separator maxHeight="-Infinity" maxWidth="-Infinity" orientation="VERTICAL" + prefHeight="24.0" prefWidth="6.0"/> + <Label maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="24.0" prefWidth="103.0" + text="Generation"/> + <Label fx:id="generationNumberLabel" alignment="CENTER_RIGHT" contentDisplay="TEXT_ONLY" + maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="24.0" prefWidth="99.0"/> + <Separator maxHeight="-Infinity" maxWidth="-Infinity" orientation="VERTICAL" + prefHeight="24.0" prefWidth="6.0"/> + </children> + </HBox> + <MatrixPane fx:id="matrixPane" alignment="CENTER" hgap="1.0" + maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="600.0" prefWidth="980.0" vgap="1.0" + AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" + AnchorPane.topAnchor="35.0"/> + </children> +</AnchorPane> diff --git a/src/main/resources/viewer/viewer.fxml b/src/main/resources/viewer/viewer.fxml deleted file mode 100644 index 93e243f8c4aa72de69ed3034b666ce3777bef6ba..0000000000000000000000000000000000000000 --- a/src/main/resources/viewer/viewer.fxml +++ /dev/null @@ -1,8 +0,0 @@ -<?import javafx.scene.layout.GridPane?> - -<?import javafx.scene.canvas.Canvas?> -<GridPane fx:controller="viewer.Controller" - xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10"> - - <Canvas fx:id="canvas" width="1200" height="900"/> -</GridPane> \ No newline at end of file diff --git a/src/test/java/mandelbrot/ComplexTest.java b/src/test/java/mandelbrot/ComplexTest.java deleted file mode 100644 index 458277c4c7e5ae08f8ce4e0cb695b3a73e879544..0000000000000000000000000000000000000000 --- a/src/test/java/mandelbrot/ComplexTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package mandelbrot; -import static net.obvj.junit.utils.matchers.AdvancedMatchers.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; - -public class ComplexTest { - private Complex onePlusI; - private Complex minusI; - private Complex minusOne; - private Complex oneMinusI; - private Complex twoI; - private Complex two; - private Complex one; - private Complex i; - private Complex zero; - - @BeforeEach - void initializeTestValues(){ - onePlusI = new Complex(1,1); - minusI = new Complex(0,-1); - minusOne = new Complex(-1,0); - oneMinusI = new Complex(1, -1); - twoI = new Complex(0,2); - two = new Complex(2,0); - one = new Complex(1,0); - i = new Complex(0,1); - zero = new Complex(0,0); - } - - @Test - void testEquals(){ - assertThat(onePlusI, is(equalTo(onePlusI))); - assertThat(onePlusI, is(equalTo(new Complex(1, 1)))); - assertThat(two, is(not(equalTo(twoI)))); - } - - @Test - void testGetReal(){ - assertThat(two.getReal(), is(closeTo(2., Helpers.EPSILON))); - assertThat(onePlusI.getReal(), is(closeTo(1., Helpers.EPSILON))); - assertThat(oneMinusI.getReal(), is(closeTo(1., Helpers.EPSILON))); - } - - @Test - void testGetImaginary(){ - assertThat(two.getImaginary(), is(closeTo(0., Helpers.EPSILON))); - assertThat(onePlusI.getImaginary(), is(closeTo(1., Helpers.EPSILON))); - assertThat(oneMinusI.getImaginary(), is(closeTo(-1., Helpers.EPSILON))); - } - @Test - void testOne(){ - assertThat(Complex.ONE.getReal(), is(closeTo(1., Helpers.EPSILON))); - assertThat(Complex.ONE.getImaginary(), is(closeTo(0., Helpers.EPSILON))); - } - @Test - void testI(){ - assertThat(Complex.I.getReal(), is(closeTo(0., Helpers.EPSILON))); - assertThat(Complex.I.getImaginary(), is(closeTo(1., Helpers.EPSILON))); - } - @Test - void testZero(){ - assertThat(Complex.ZERO.getReal(), is(closeTo(0., Helpers.EPSILON))); - assertThat(Complex.ZERO.getImaginary(), is(closeTo(0., Helpers.EPSILON))); - } - @Test - void testNegate(){ - assertThat(two.negate(), is(equalTo(new Complex(-2,0)))); - assertThat(minusI.negate(), is(equalTo(i))); - assertThat(oneMinusI.negate(), is(equalTo(new Complex(-1, 1)))); - } - - @Test - void testReciprocal(){ - assertThat(one.reciprocal(), is(equalTo(one))); - assertThat(minusI.reciprocal(), is(equalTo(i))); - assertThat(two.reciprocal(), is(equalTo(new Complex(0.5,0)))); - assertThat(oneMinusI.reciprocal(), is(equalTo(new Complex(0.5,0.5)))); - } - - @Test - void testReciprocalOfZero(){ - assertThat(()->zero.reciprocal(), throwsException(ArithmeticException.class)); - } - - @Test - void testSubtract(){ - assertThat(zero.subtract(one), is(equalTo(minusOne))); - assertThat(one.subtract(i), is(equalTo(oneMinusI))); - } - - @Test - void testDivide(){ - assertThat(onePlusI.divide(Complex.ONE), equalTo(onePlusI)); - assertThat(Complex.ONE.divide(two), equalTo(new Complex(0.5, 0))); - assertThat(oneMinusI.divide(onePlusI), equalTo(minusI)); - } - - @Test - void testDivideByZero(){ - assertThat(()->one.divide(zero), throwsException(ArithmeticException.class)); - } - - @Test - void testConjugate(){ - assertThat(two.conjugate(), equalTo(two)); - assertThat(oneMinusI.conjugate(), equalTo(onePlusI)); - } - - @Test - void testRotation(){ - assertThat(Complex.rotation(Math.PI/2), equalTo(i)); - assertThat(Complex.rotation(-Math.PI/2), equalTo(minusI)); - assertThat(Complex.rotation(0), equalTo(one)); - assertThat(Complex.rotation(Math.PI/4), equalTo(new Complex(Math.sqrt(2)/2., Math.sqrt(2)/2.))); - assertThat(Complex.rotation(Math.PI/3), equalTo(new Complex(1./2., Math.sqrt(3)/2.))); - } - - @Test - void testBasicToString(){ - assertThat(two.toString(), containsString("2.0")); - assertThat(i.toString(), containsString("i")); - } - - @Test - void testToStringFormat(){ - assertThat(oneMinusI.toString(), is(equalTo("1.0 - 1.0i"))); - assertThat(onePlusI.toString(), is(equalTo("1.0 + 1.0i"))); - assertThat(minusI.toString(), is(equalTo("-1.0i"))); - assertThat(twoI.toString(), is(equalTo("2.0i"))); - assertThat(two.toString(), is(equalTo("2.0"))); - } -} \ No newline at end of file diff --git a/src/test/java/model/GridTest.java b/src/test/java/model/GridTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a2956612e6cd8c13ead11e6e77623b957964c994 --- /dev/null +++ b/src/test/java/model/GridTest.java @@ -0,0 +1,40 @@ +package model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class GridTest { + private Grid grid; + + @BeforeEach + public void initializeGrid(){ + grid = new Grid(3,3); + } + + @Test + public void testGetNeighbours(){ + assertThat(grid.getNeighbours(1,1), is(notNullValue())); + assertThat(grid.getNeighbours(1,1), hasSize(equalTo(8))); + assertThat(grid.getNeighbours(1,1), + containsInAnyOrder(grid.getCell(0,0), + grid.getCell(0,1), + grid.getCell(0,2), + grid.getCell(1,0), + grid.getCell(1,2), + grid.getCell(2,0), + grid.getCell(2,1), + grid.getCell(2,2))); + } + + @Test + public void testCountAliveNeighbours(){ + assertThat(grid.countAliveNeighbours(1,1), is(equalTo(0))); + grid.getCell(2,2).setState(CellState.ALIVE); + grid.getCell(0,0).setState(CellState.ALIVE); + assertThat(grid.countAliveNeighbours(1,1), is(equalTo(2))); + } + + +}