Skip to content
Snippets Groups Projects
Commit 31ed353a authored by LABOUREL Arnaud's avatar LABOUREL Arnaud
Browse files

Première version template jeu de la vie

parent c3aaad1c
No related branches found
No related tags found
No related merge requests found
Showing
with 639 additions and 740 deletions
# 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
......
......@@ -23,5 +23,5 @@ test {
}
application {
mainClassName = "viewer.Main"
mainClassName = "GameOfLifeApplication"
}
\ No newline at end of file
rootProject.name = 'mandelbrot'
rootProject.name = 'game-of-life'
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();
}
}
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);
}
}
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();
}
});
}
}
});
}
}
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";
}
}
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;
}
}
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 -&gt; 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;
}
}
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;
}
}
}
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;
}
}
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;
}
}
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);
}
}
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) {
}
}
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;
}
}
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());
}
}
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));
}
}
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);
}
}
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;
}
}
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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment