diff --git a/.gitignore b/.gitignore index c6792f3..20ef7c1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ we-get-these-100s.iml latex/main.aux latex/main.log .DS_Store +main.out .env \ No newline at end of file diff --git a/README.md b/README.md index 3ec44c5..6868cc7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ # WE GET THESE 100S +This simulation project integrates multiple components to create a dynamic ecosystem that a researcher can use to +study predator-prey-plant interactions, with the ability to add or remove any species or environmental factors easily +by editing the given JSON file. Infact, almost every aspect of the simulation can be controlled in the JSON file. +The simulation is designed to simulate without any grid-based restrictions, allowing entities to move freely in a +continuous space. Specifically, the coordinates of the entities are stored as doubles from zero to the width and height +of the field. The entities also have their own genetics, which are inherited from parent(s) and may mutate, +all of which are initialised in the JSON file, where it is intended for all users of the program to modify it +to play with the simulation parameters. + +Authors: Mehmet Kutay Bozkurt and Anas Ahmed +Version: 1.0 + +--- + [Google Docs](https://docs.google.com/document/d/14-HuoK5tUVpEVj2pL6A5acK-5UiRciUfZDvJvdfa1uY/edit) To open the GUI, add port 6080 under the "Ports" section and open it in the browser. diff --git a/latex/main.pdf b/latex/main.pdf index 1fb3ec3..fd4e46f 100644 Binary files a/latex/main.pdf and b/latex/main.pdf differ diff --git a/latex/main.tex b/latex/main.tex index 4cc9da3..884be73 100644 --- a/latex/main.tex +++ b/latex/main.tex @@ -1,85 +1,110 @@ -\documentclass[12pt, a4paper]{scrartcl} +\documentclass[10pt, a4paper]{scrartcl} \renewcommand{\baselinestretch}{1.15} \usepackage{changepage} \usepackage{multicol} \usepackage{paralist} \usepackage{listings} -\usepackage[lmargin=0.9in, rmargin=0.9in, tmargin=1in, bmargin=1.3in]{geometry} +\usepackage{amsmath} +\usepackage{hyperref} +\usepackage[lmargin=0.8in, rmargin=0.8in, tmargin=1in, bmargin=1.3in]{geometry} \setkomafont{disposition}{\normalfont\bfseries} \parskip=8pt \begin{document} -\pagenumbering{gobble} - -\begin{titlepage} - \begin{center} - \LARGE - \textbf{KING'S COLLEGE LONDON} - - \vspace{2cm} - - \begin{adjustwidth}{-1cm}{-1cm} - \centering - \Large - \textbf{4CCS1PPA PROGRAMMING PRACTICE AND APPLICATIONS} - \end{adjustwidth} - - \vspace{0.5cm} - - \Large - \textbf{Third "Simulation" Coursework (Feb 2025)} - - \vspace{2cm} - - \Large - Project Name: The Simulation - - \vspace{1cm} - - \Large - \begin{tabular}{l l} - Student Name: & Mehmet Kutay Bozkurt \\ - Student ID: & 23162628 \\ - \vspace{0.5cm} & \\ - Student Name: & Anas Ahmed \\ - Student ID: & xxxxxxxx - \end{tabular} - \end{center} - % \tableofcontents -\end{titlepage} - -\begin{multicols}{2} - -\pagenumbering{arabic} - -\section{Introduction} - -\noindent This simulation project integrates multiple components to create a dynamic ecosystem that a researcher can use to -study predator-prey-plant interactions, with the ability to add or remove any species or environmental factors easily -by editing the given JSON file. -The simulation is designed to simulate without any grid-based restrictions, allowing entities to move freely in a -continuous space. Specifically, the coordinates of the entities are stored as doubles from zero to the width and height -of the field. - -\noindent The simulation smoothly runs at 60 frames per second by utilising a \verb|QuadTree| for storing entities, which -allows the entity searching and collision detection to be highly optimised. Additionally, in each simulation "step," -every entity is "updated" by calling its \verb|update()| method, which handles movement, reproduction, and hunger. -Entities make decisions based on other entities in their vicinity (that is, entities that are located inside their -\verb|sight| radius) and the current state of the environment. Additionally, there is a -method to handle overcrowding in the simulation, for limiting the exponentioal growth of entities. - -\section{Tasks Lists and Implementation Details} - -\subsection{Base Tasks} - - \noindent \textbf{Diverse Species:} With how the simulation is implemented, adding new species is as easy as adding - the species' behavioural data into a JSON file. Each entity species can be of type \verb|Prey|, \verb|Predator|, - or \verb|Plant|, and the data is added accordingly. For example, for the predator \verb|Fox|, the following definitions - is used: - \begin{verbatim} + \pagenumbering{gobble} + + \begin{titlepage} + \begin{center} + \LARGE + \textbf{KING'S COLLEGE LONDON} + + \vspace{2cm} + + \begin{adjustwidth}{-1cm}{-1cm} + \centering + \Large + \textbf{4CCS1PPA PROGRAMMING PRACTICE AND APPLICATIONS} + \end{adjustwidth} + + \vspace{0.5cm} + + \Large + \textbf{Third "Simulation" Coursework (Feb 2025)} + + \vspace{2cm} + + \Large + Project Name: The Simulation + + \vspace{1cm} + + \Large + \begin{tabular}{l l} + Student Name: & Mehmet Kutay Bozkurt \\ + Student ID: & 23162628 \\ + \vspace{0.5cm} & \\ + Student Name: & Anas Ahmed \\ + Student ID: & 23171444\end{tabular} + \end{center} + % \tableofcontents + \end{titlepage} + + \begin{multicols}{2} + + \pagenumbering{arabic} + + \section{Introduction} + \noindent This simulation project integrates multiple components to create a dynamic ecosystem that a researcher can use to + study predator-prey-plant interactions, with the ability to add or remove any species or environmental factors easily + by editing the given JSON file. Infact, almost every aspect of the simulation can be controlled in the JSON file. + The simulation is designed to simulate without any grid-based restrictions, allowing entities to move freely in a + continuous space. Specifically, the coordinates of the entities are stored as doubles from zero to the width and height + of the field. The entities also have their own genetics, which are inherited from parent(s) and may mutate, + all of which are initialised in the JSON file, where it is intended for all users of the program to modify it + to play with the simulation parameters. + + \noindent The simulation smoothly runs at 60 frames per second by utilising a \verb|QuadTree| to store entities, which allows + the entity searching and collision detection to be highly optimised. In each simulation "step," every entity is updated + by calling its \verb|update()| method, which handles movement, reproduction, and hunger. + Entities make decisions based on other entities in their vicinity (that is, entities that are located inside their + \verb|sight| radius) and the current state of the environment (the weather and the time of day). Additionally, there is a + method to handle overcrowding in the simulation, for limiting the growth of entities from being unnaturally rapid. + + \section{Directions for Use} + + Since the final code base is quite different from what was given as the base code for the project, it is necessary to + provide a new set of instructions for running the simulation. Firstly, the \verb|Engine| class is responsible for + starting and setting up the simulation. A new \verb|Engine| object can be created as + \vspace{-0.1cm} + \begin{verbatim} +Engine engine + = new Engine(WIDTH, HEIGHT, FPS); + \end{verbatim} + \vspace{-0.7cm} + where \verb|WIDTH| and \verb|HEIGHT| are the dimensions of the + display window and \verb|FPS| is the frame rate of the simulation. The simulation can then be started by calling the + \verb|start()| method on the \verb|Engine| object. + + \noindent Additionally, these have been already set up in the \verb|Main| class, so the simulation can be run by simply running + the \verb|main()| method under it. Since the parameters of the simulation is read from the JSON file, it is necessary + to have the \verb|simulation_data.json| file in the root directory of the project (the place where the \verb|Main.java| + file can be found), but this is already provided in the Jar file in the submission. Finally, the simulation uses an + external package called \href{https://github.com/google/gson/}{Gson} to read the + JSON file, and which is why the package is bundled in the Jar file in the submission. + + \section{Tasks Lists and Implementation Details} + + \subsection{Base Tasks} + + \noindent \textbf{Diverse Species:} With how the simulation is implemented, adding new species is as easy as adding + the species' behavioural data into a JSON file. Each entity species can be of type \verb|Prey|, \verb|Predator|, + or \verb|Plant|, and the data is added accordingly. For example, for the predator \verb|Fox|, the following definitions + are used: + \vspace{-0.1cm} + \begin{verbatim} { "name": "Fox", "multiplyingRate": [0.05, 0.15], @@ -97,49 +122,191 @@ \subsection{Base Tasks} "overcrowdingRadius": [10, 15], "maxOffspringSpawnDistance": [3, 5] } - \end{verbatim} - \noindent The values that are arrays that contain two values (such as \verb|sight|, \verb|size|, or \verb|maxSpeed|) - are the minimum and the maximum values that the entity can have for that genetic trait. The values that are not - arrays are the fixed values for that genetic trait. In addition, these values can mutate when the entity breeds - (for animals) or multiplies (for plants). - - - - \noindent \textbf{Two Predators Competes for the Same Food Source:} - Since it is easy to add multiple species of predators, currently there are two predators, \verb|Wolf| and \verb|Fox|, - that compete for the same food source, \verb|Rabbit|. - - \noindent \textbf{Distinguishing Gender:} Each entity has their own gender, represented in their genetics, - which affects reproduction mechanics. Only entities of opposite genders can reproduce. - - \noindent \textbf{Tracking Time of Day:} A simulation clock governs the day/night cycle impacting entity behaviour. - -\subsection{Challenge Tasks} - - \noindent \textbf{Adding Plants:} Plants have been added, featuring growth and reproduction dynamics. - - \noindent \textbf{Adding Weather:} Weather conditions and time of day influence behavior and visibility, - creating a \textit{realistic} simulation environment. - - \noindent \textbf{Disease Dynamics:} - -\section{Code Quality Considerations} - -\subsection{Coupling} - - -\subsection{Cohesion} - - -\subsection{Responsibility-Driven Design} - - -\subsection{Maintainability} - - -\section{Final Remarks} - - -\end{multicols} + \end{verbatim} + \vspace{-0.6cm} + \noindent The values that are arrays and contain two values (such as \verb|sight|, \verb|size|, or \verb|maxSpeed|) represent + the minimum and the maximum values that the entity can have for that genetic trait. In addition, these values can mutate when + the entity breeds/multiplies. However, \verb|numberOfEntitiesAtStart| and \verb|eats| are fixed values that are not subject + to mutation, as they are not genetic traits and they define what the entity is in the context of the simulation. + Finally, the following entities are considered in the simulation: + \begin{itemize} + \setlength\itemsep{-0.25em} + \item \verb|Grass| — Plant. + \item \verb|Rabbit| — Prey, eats grass. + \item \verb|Squirrel| — Prey, eats grass. + \item \verb|Wolf| — Predator, eats rabbit and squirrel. + \item \verb|Fox| — Predator, eats rabbit. + \item \verb|Bear| — Predator, eats wolf and fox. + \end{itemize} + + \noindent \textbf{Two Predators Competes for the Same Food Source:} + With the JSON configuration file, it is quite easy to add multiple species for any type of entity. In this case, only two + predators (\verb|Wolf| and \verb|Fox|) compete for the same food source (\verb|Rabbit|), which is a prey species. + The \verb|Bear| is also added as a predator that eats both wolves and foxes. + + \noindent \textbf{Distinguishing Gender:} Each animal has their own gender, represented in their genetics, + which affects reproduction mechanics. Only animals of opposite genders can reproduce. Specifically, the gender genetic trait + is implemented as an Enum with two values: \verb|MALE| and \verb|FEMALE|. Plants do not have gender in the genetics system, + meaning that they reproduce asexually. + + \noindent \textbf{Tracking Time of Day:} An \verb|Environment| class is used to track the time of day and the weather, + which governs how both cycles impact entity behaviour. During the night, entities will not move unless they are hungry + or there is a predator nearby. Additionally, when sleeping, food consumption is reduced. The day-night cycle can be controlled + by the JSON file, and the time of day is displayed on the screen. Additionally, as the day progresses into the night, + the screen darkens to represent the time of day, without affecting the text on the screen. + + \subsection{Challenge Tasks} + + \noindent \textbf{Adding Plants:} Plants have been added, featuring growth and reproduction dynamics. + Plants die when they detect too many plants of the same species nearby (as determined by their overcrowding genetics: + \verb|overcrowdingThreshold| and \verb|overcrowdingRadius|), which results in natural looking patches of grass. + + \noindent \textbf{Adding Weather:} As mentioned, weather is added under the \verb|Environment| class, wherein + weather conditions influence behavior and visibility, increasing the realism in the simulation environment. + There are 4 weather conditions: + \begin{itemize} + \setlength\itemsep{-0.25em} + \item \verb|Clear| — No effect on entities. + \item \verb|Raining| — Plants grow faster (by a defined factor in the plants' genetics). + \item \verb|Windy| — Pushes entities in the wind direction, even when they are sleeping. + Wind direction is also visualised for the ease of the user. + \item \verb|Storm| — Slows down entities by some factor and has the effect of windy. Different to + windy condition, stormy condition is more severe, in the sense that the wind changes directions much more rapidly. + \end{itemize} + + \noindent \textbf{Genetics System:} As one of the self-admitted challenges, a genetics system for all of the entities + was implemented. As mentioned earlier in the first base task, when reproducing, animals combine their parents' genetics + to form their own, with a chance to mutate certain attributes by some mutation factor. Specifically, if \( r \in [0, 1] \) + is a random number, then the new genetic trait is calculated as: + \begin{equation} + \text{value} = \text{fatherTrait} \times r + \text{motherTrait} \times (1 - r), \nonumber + \end{equation} + allowing for a smooth transition between the parents' traits. Then, if we define \( s \in \{-1, 1\} \) to be a random + value, the mutation factor is applied to the new trait as follows: + \begin{equation} + \label{mutation-equation} + \text{newTrait} = \text{value} + \text{value} \times \text{mutationFactor} \times s, + \end{equation} + where the mutation factor is a value, in the range \( [0, 1] \), that determines how drastic the mutation is. + This system allows for a wide range of genetic diversity in the simulation, which is easily observable when + the \verb|mutationFactor| is increased. Lastly, plants reproduce asexually, so they inherit their parent's genetics + directly, but these can also mutate according to Equation \ref{mutation-equation}. + + \noindent \textbf{JSON Configuration File: } Almost every single aspect of the simulation is controlled from this file, + including the entities' genetics, the environment, and the simulation parameters. The JSON file is loaded at the start + of the simulation, by utilising the external package \href{https://github.com/google/gson/}{Gson}. Then, the simulation + is run according to the parameters defined in the file. As well as the entity + genetic intervals mentioned above, the following are the parameters that can be controlled in the JSON file: + \begin{itemize} + \setlength\itemsep{-0.25em} + \item \verb|foodValueForAnimals| — Scales the food value when an entity eats an animal. + \item \verb|foodValueForPlants| — Scales the food value when entity eats a plant. + \item \verb|animalHungerDrain| — Controls the rate of hunger drain over time. + \item \verb|animalBreedingCost| — Scales how much food is consumed during breeding (note that food is a value + in the range 0 to 1). + \item \verb|mutationFactor| — How drastic the mutation changes are. + \item \verb|entityAgeRate| — How fast entities age. + \item \verb|fieldScaleFactor| — The size of the field, smaller value means more zoomed in. + \item \verb|weatherChangeProbability| — The probability of the weather changing at the end of the day. + \item \verb|windStrength| — How strong the wind pushes entities. + \item \verb|stormMovementSpeedFactor| — How much to slow entities during a storm. + \item \verb|dayNightCycleSpeed| — How fast the time passes in the simulation. + \item \verb|doDayNightCycle| — Whether the day-night cycle is enabled. + \item \verb|doWeatherCycle| — Whether the weather is enabled. + \item \verb|showQuadTrees| — Whether to show the debug effect of quadtrees. It just looks really cool. + \item \verb|animalHungerThreshold| — The level of food level when an animal is considered "hungry." + \item \verb|animalDyingOfHungerThreshold| — The level of food level when an animal is considered to be "dying of hunger." + \end{itemize} + + \noindent \textbf{Quadtree Optimisation: } While not a visible feature, due to its complexity, it is worth discussing. + A quadtree is utilised to store entities, instead of a list. This is done to efficiently handle entity proximity checks. Each frame, + entities are added to the quadtree, and when they need to find nearby entities, they query the tree. This is is a major upgrade + to the naïve approach of $O(n^2)$ time-complexity, where every entity checks its distance to every other entity (1000 entities means + 1,000,000 calls, 60 times a second). Using the quadtree for every entity has an average complexity of $O(n \log n)$, since the + quadtree organises entities by proximity. Overall, this greatly improves performance, making the experience of the simulation + much smoother. + + \noindent \textbf{Graphics: } Some additional visual effects were also included, such as different shapes for different entity types + (squares for predators, circles for preys, and triangles for plants), as well as rain and lightning, and a day-night darkening effect + (just to make the weather more visual). There is also some text describing the time, day, weather, wind direction (when windy or + stormy), and current entity count for each type of species. + + \section{Code Quality Considerations} + + \subsection{Coupling and Responsibility-Driven Design} + \noindent Coupling is minimised by separating all major functions into different classes. The class structure of the + program starts with the \verb|Engine| class. The \verb|Engine| controls the main loop of the simulation, combining the actual + simulation and the graphics together. + + \noindent The \verb|Engine| only stores the \verb|Display| for graphics, the \verb|Simulator| for updating the simulation and a + \verb|Clock| class for maintaining the frame rate. The \verb|Engine| only coordinates these classes, it does not handle their + internal logic, meaning it has minimal coupling. + + \noindent Graphics are handled by the \verb|Display| and the \verb|RenderPanel| classes. The \verb|RenderPanel| class + extends the \verb|JPanel| class and is responsible for rendering the simulation, and since it has low coupling, it is + easy to change the \verb|RenderPanel| class to render the simulation on different mediums — a web browser, for example. + Furthermore, the simulation is controlled by the + \verb|Simulator| class. All entities are stored in a \verb|Field| class, which is created by a \verb|FieldBuilder| class to + move the population of the \verb|Field| outside of the \verb|Field| class, improving Responsibility-Driven Design and + minimising coupling. + + \noindent All of the animals and plants in the simulation originate from an \verb|Entity| class. This then has + \verb|Animal| and \verb|Plant| as subclasses, and \verb|Animal| has \verb|Predator| and \verb|Prey| as subclasses. + It is ensured that any method or attribute shared by any class is stored in their respective parent class to reduce + code duplication and enforce Responsibility-Driven Design for each subclass. The subclasses strictly only do things + that they do differently from other sibling classes. + + \noindent Also, as mentioned above, the \verb|Entity| class has an \verb|update()| method which is called in the + \verb|Simulator| class. This minimises coupling as all entity specific behavior remains encapsulated, while remaining + easy to invoke. Additionally this \verb|update()| method is overridden in the subclasses to allow for different + behavior for each type of entity, while functionality that every entity shares is called from the parent class, such as + the \verb|incrementAge()| method. + + \noindent Finally, only necessary dependencies are stored between classes, making the code base low in coupling. + Naturally, as Responsibility-Driven Design was strictly followed for every class, the coupling is further reduced. + + \subsection{Cohesion} + \noindent To improve cohesion, the \verb|Animal| class has four main attributes: + \begin{itemize} + \setlength\itemsep{-0.25em} + \item \verb|AnimalMovementController|, + \item \verb|AnimalHungerController|, + \item \verb|AnimalBreedingController|, and + \item \verb|AnimalBehaviourController|. + \end{itemize} + Each of these controllers + control their respective function of the \verb|Animal| class. Similar to the controllers of the \verb|Animal| class, controllers + for the \verb|Environment| class are also created, the \verb|WeatherController| and the \verb|TimeController|. + This massively increases cohesion as the different operations + are split into different relevant sections, making the code much easier to understand. This also has the added benefit of making + the \verb|Animal| and \verb|Environment| classes quite small, and improves responsibility driven design and code readability. + + \noindent The structure of \verb|Entity| and its subclasses also lends itself to high cohesion — its extremely clear what an + \verb|Animal| should do, and what a \verb|Predator| and \verb|Prey| does differently while also inherintly being subclasses of + \verb|Animal|. This once again decreases code duplication and increases the cohesion of the code base. + + \noindent + + \subsection{Maintainability} + \noindent The JSON file and genetics system is highly modular, allowing easy modifications by updating only the JSON file and either the + \verb|SimulationData|, \verb|AnimalData|, or \verb|PlantData| classes accordingly. + + \noindent Code is structured into packages, improving organisation and making it easier to locate, modify, and extend + functionality while maintaining encapsulation. The code base is written with low coupling and adheres to Responsibility-Driven Design, + resulting in high modularity. This means adding functionality is quite straightforward, without needing to change other parts + of the code. Modular code also makes unit testing very simple, as demonstrated by the unit test classes in the code base, which + were highly effective in testing the code and ensuring it was working as expected as the code was developed. + + \noindent One aspect that may prove difficult is making the simulation deterministic. The simulation is not deterministic + and runs differently each time, as the random number generation system uses different instances of \verb|Math.random()| throughout + the code. This is one aspect that could be improved upon by utilising a single random number generator instance. + + \section{Final Remarks} + \noindent This project implements a dynamic, modular and optimised simulation of an ecosystem. The system expands and goes + beyond the base tasks, completely reworking and improving the original code. It is hoped that the simulation is both + educational, and enjoyable to use and watch, while modifying the JSON file to see how different parameters affect which + species thrive and which do not. + + \end{multicols} \end{document} \ No newline at end of file diff --git a/src/Main.java b/src/Main.java index 5d6b30f..a98599f 100644 --- a/src/Main.java +++ b/src/Main.java @@ -1,9 +1,17 @@ import view.Engine; +/** + * Main class to start the simulation through the Engine class. + * + * @author Mehmet Kutay Bozkurt and Anas Ahmed + * @version 1.0 + */ public class Main { public static void main(String[] args) { int fps = 60; - Engine engine = new Engine(600, 600, fps); + int width = 600; + int height = 600; + Engine engine = new Engine(width, height, fps); engine.start(); } } diff --git a/src/entities/Plant.java b/src/entities/Plant.java index ce0c492..8cad924 100644 --- a/src/entities/Plant.java +++ b/src/entities/Plant.java @@ -1,5 +1,6 @@ package entities; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -11,7 +12,8 @@ import util.Vector; /** - * A class that holds the properties of a plant entity. + * A class that holds the properties of a plant entity. Plants can multiply and + * spread their seeds (basically new plants). * * @author Anas Ahmed and Mehmet Kutay Bozkurt * @version 1.0 @@ -28,8 +30,8 @@ public Plant(PlantGenetics genetics, Vector position) { } /** - * Spawn new plants around this plant. The new plants have the same - * genetics as the parent plant (may be mutated). + * Spawn new plants around this plant. The new plants have the same genetics + * as the parent plant, though it may be mutated. * @return A list of new plants if the plant can multiply, empty list otherwise. */ public List multiply(Field field) { @@ -39,38 +41,40 @@ public List multiply(Field field) { if (!(canMultiply() && Math.random() < multiplyingRate)) return Collections.emptyList(); - if (!field.environment.isDay()){ // If it's night, very low odds of multiplying + if (!field.environment.isDay()) { // If it's night, very low odds of multiplying. if (Math.random() > 0.3) { return Collections.emptyList(); } } - // Growth factor affects number of seeds and range of growth + // Growth factor affects number of seeds and range of growth: int seeds = (int) (genetics.getNumberOfSeeds() * growthFactor); - Plant[] newPlants = new Plant[seeds]; + List newPlants = new ArrayList<>(); for (int i = 0; i < seeds; i++) { Vector seedPos = position.getRandomPointInRadius(genetics.getMaxOffspringSpawnDistance() + growthFactor - 1); - newPlants[i] = new Plant(genetics.getOffspringGenetics(), seedPos); + newPlants.add(new Plant(genetics.getOffspringGenetics(), seedPos)); } - return List.of(newPlants); + return newPlants; } /** - * Update this plant entity. + * Update this plant entity. This includes multiplying and handling overcrowding. */ @Override public void update(Field field, double deltaTime) { if (!isAlive()) return; super.update(field, deltaTime); + List nearbyEntities = searchNearbyEntities(field, genetics.getOvercrowdingRadius()); + List newPlants = multiply(field); for (Plant plant : newPlants) { field.putInBounds(plant, plant.getSize()); field.addEntity(plant); } - handleOvercrowding(field); + handleOvercrowding(nearbyEntities); } /** @@ -84,6 +88,7 @@ public void draw(Display display, double scaleFactor) { size = Math.max(2, size); int x = (int) (position.x() / scaleFactor); int y = (int) (position.y() / scaleFactor); + display.drawEqualTriangle(x, y, size, genetics.getColour()); } } \ No newline at end of file diff --git a/src/entities/PlantTest.java b/src/entities/PlantTest.java index a69d4d8..3306666 100644 --- a/src/entities/PlantTest.java +++ b/src/entities/PlantTest.java @@ -11,6 +11,12 @@ import util.Vector; import genetics.PlantGenetics; +/** + * Test class for the Plant entity. Tests methods underneath it. + * + * @author Mehmet Kutay Bozkurt and Anas Ahmed + * @version 1.0 + */ class PlantTest { private PlantGenetics genetics; private Field field; diff --git a/src/entities/Predator.java b/src/entities/Predator.java index 2426632..654b063 100644 --- a/src/entities/Predator.java +++ b/src/entities/Predator.java @@ -6,7 +6,7 @@ import util.Vector; /** - * An arbitrary predator entity. Predators move around randomly and can reproduce. + * A class for representing an arbitrary predator entity. * * @author Mehmet Kutay Bozkurt and Anas Ahmed * @version 1.0 @@ -31,7 +31,6 @@ public void draw(Display display, double scaleFactor) { int x = (int) ((position.x() - (double) size / 2) / scaleFactor); // Draw rectangle centered around x, y of predator. int y = (int) ((position.y() - (double) size / 2) / scaleFactor); - // drawSightRadius(display, scaleFactor); display.drawRectangle(x, y, size * 2, size * 2, genetics.getColour()); } diff --git a/src/entities/Prey.java b/src/entities/Prey.java index 6d8d400..645a481 100644 --- a/src/entities/Prey.java +++ b/src/entities/Prey.java @@ -6,7 +6,7 @@ import util.Vector; /** - * An arbitrary prey entity that moves around randomly and can reproduce. + * A class for representing an arbitrary prey entity. * * @author Anas Ahmed and Mehmet Kutay Bozkurt * @version 1.0 @@ -30,6 +30,7 @@ public void draw(Display display, double scaleFactor) { size = Math.max(1, size); int x = (int) (position.x() / scaleFactor); int y = (int) (position.y() / scaleFactor); + display.drawCircle(x, y, size, genetics.getColour()); } diff --git a/src/entities/generic/Animal.java b/src/entities/generic/Animal.java index 0d2bf26..004e2cc 100644 --- a/src/entities/generic/Animal.java +++ b/src/entities/generic/Animal.java @@ -9,23 +9,22 @@ import simulation.simulationData.Data; /** - * Abstract class for all animals in the simulation. - * Contains controllers for moving, breeding, handling hunger, and the behaviour - * of the animal. + * Abstract class for all animals in the simulation. Contains controllers for + * moving, breeding, handling hunger, and the behaviour of the animal. Contains + * the update method to handle all the controllers and update the behaviour of the animal. * * @author Mehmet Kutay Bozkurt and Anas Ahmed * @version 1.0 */ public abstract class Animal extends Entity { - protected AnimalGenetics genetics; // Re-cast to AnimalGenetics + protected AnimalGenetics genetics; // Re-cast to AnimalGenetics. Stores the genetics of this animal. - protected boolean isMovingToMate = false; // Stores if the animal is currently attempting to mate - protected boolean isAsleep = false; // Stores if the animal is currently asleep + protected boolean isAsleep = false; // Stores if the animal is currently asleep. - protected final AnimalMovementController movementController; // Controller for moving the animal - protected final AnimalBreedingController breedingController; // Controller for breeding the animal - protected final AnimalHungerController hungerController; // Controller for handling hunger - protected final AnimalBehaviourController behaviourController; // Controller for handling decision logic of animals + protected final AnimalMovementController movementController; // Controller for moving the animal. + protected final AnimalBreedingController breedingController; // Controller for reproduction. + protected final AnimalHungerController hungerController; // Controller for handling hunger. + protected final AnimalBehaviourController behaviourController; // Controller for handling decision logic of animals. /** * Constructor -- Create a new Animal. @@ -49,32 +48,57 @@ public void update(Field field, double deltaTime) { if (!isAlive()) return; super.update(field, deltaTime); + // Cache the nearby entities to avoid multiple searches over the field, which would slow down the simulation. List nearbyEntities = searchNearbyEntities(field, genetics.getSight()); + // Delagate the breeding to the controller. If breeding occurs, add the offspring to the field, + // otherwise the list will be empty. List newEntities = breedingController.breed(nearbyEntities); for (Animal entity : newEntities) { field.putInBounds(entity, entity.getSize()); field.addEntity(entity); } - handleOvercrowding(field); + handleOvercrowding(nearbyEntities); hungerController.handleHunger(deltaTime, newEntities.size()); - if (field.environment.getWeather() == Weather.WINDY || field.environment.getWeather() == Weather.STORM) { - Vector windVector = field.environment.getWindVector().multiply(Data.getWindStrength()); - setPosition(position.add(windVector)); - } + windyCondition(field); // Handle windy condition. Vector lastPosition = position; movementController.setLastPosition(lastPosition); // Update last position before moving. + + // Update the behaviour of the animal, according to the nearby entities and the field: behaviourController.updateBehaviour(field, nearbyEntities, deltaTime); + stormyCondition(field, lastPosition); // Handle stormy condition. + } + + /** + * Handle windy condition for the animal, moving it in the direction of the wind. + * @param field The field the animal is in. Used to get access the environment. + */ + private void windyCondition(Field field) { + // If wind is present, move the animal in the direction of the wind. + if (field.environment.getWeather() == Weather.WINDY || field.environment.getWeather() == Weather.STORM) { + // Wind strength is applied here: + Vector windVector = field.environment.getWindVector().multiply(Data.getWindStrength()); + this.setPosition(position.add(windVector)); + } + } + + /** + * Handle stormy condition for the animal, hindering its movement. + * @param field The field the animal is in. Used to get access the environment. + * @param lastPosition The last position of the animal to calculate the speed. + */ + private void stormyCondition(Field field, Vector lastPosition) { // Hinder the speed of the animal if it is stormy: if (field.environment.getWeather() == Weather.STORM) { Vector differenceVector = position.subtract(lastPosition); double speed = differenceVector.getMagnitude(); - speed *= Data.getStormMovementSpeedFactor() / ((double) getSize() / 4); + // The speed is decreased by the storm movement speed factor and the size of the animal: + speed *= Data.getStormMovementSpeedFactor() / (getSize() / 4d); setPosition(lastPosition.add(differenceVector.multiply(speed))); } } @@ -89,7 +113,7 @@ public void update(Field field, double deltaTime) { * Used for a rather specific use case in prey detecting predators that can eat them, * so that the prey can run away. */ - public AnimalHungerController getHungerController() { - return hungerController; + public boolean canEat(Entity entity) { + return hungerController.canEat(entity); } } \ No newline at end of file diff --git a/src/entities/generic/AnimalBehaviourController.java b/src/entities/generic/AnimalBehaviourController.java index f0b9b88..036df32 100644 --- a/src/entities/generic/AnimalBehaviourController.java +++ b/src/entities/generic/AnimalBehaviourController.java @@ -1,20 +1,21 @@ package entities.generic; -import entities.Predator; import simulation.Field; import java.util.List; -import java.util.function.Predicate; /** * Handles animal behaviour. All animal behaviour, predator or prey, is identical. - * They move to food and away from things that eat them. + * They move to food and away from things that eat them. They try to move to + * mates when they are not hungry. They sleep at night if they are not hungry. * * @author Anas Ahmed and Mehmet Kutay Bozkurt * @version 1.0 */ public class AnimalBehaviourController { - private final Animal animal; // The animal this controller is controlling + private final Animal animal; // The animal this controller is controlling. + + private boolean isMovingToMate = false; // Stores if the animal is currently attempting to mate. /** * Constructor. @@ -34,48 +35,36 @@ public void updateBehaviour(Field field, List nearbyEntities, double del animal.isAsleep = false; boolean isHungry = animal.hungerController.isHungry(); + // Extreme case for prey to prioritise food over fleeing from predators: boolean isDyingOfHunger = animal.hungerController.isDyingOfHunger(); if (isHungry) animal.hungerController.eat(nearbyEntities); - if (!isDyingOfHunger) { // If not dying of hunger, attempt to flee from predators. + // If not dying of hunger and not moving to mate, attempt to flee from predators. + if (!isDyingOfHunger && !isMovingToMate) { // If fleeing, stop other behaviour: - if (fleeFromPredators(nearbyEntities, deltaTime)) return; + if (animal.movementController.fleeFromPredators(nearbyEntities, deltaTime)) return; } - // If not hungry, its nighttime and no predator is nearby, sleep (do nothing): - if (!isHungry && !field.environment.isDay()) { + // If not hungry, its nighttime, isn't moving to mate and no predator is nearby, sleep (do nothing): + if (!isHungry && !field.environment.isDay() && !isMovingToMate) { animal.isAsleep = true; - return; // Stop other behaviour from occuring + return; // Stop other behaviour from occurring. } boolean movingToFood = false; - if (isHungry && !animal.isMovingToMate) { // If is hungry and not currently attempting to mate + // If is hungry and not currently attempting to mate, move to food: + if (isHungry && !isMovingToMate) { movingToFood = animal.movementController.moveToNearestFood(nearbyEntities, deltaTime); } - if (!movingToFood) { // If not moving to food and not hungry, look for mate - animal.isMovingToMate = animal.movementController.moveToNearestMate(nearbyEntities, deltaTime); - if (!animal.isMovingToMate) { // If can't find mate, wander. + // If not moving to food and not hungry, look for mate: + if (!movingToFood) { + isMovingToMate = animal.movementController.moveToNearestMate(nearbyEntities, deltaTime); + if (!isMovingToMate) { // If can't find mate, just wander. animal.movementController.wander(field, deltaTime); } } } - - /** - * Searches for nearby predators and flees if found. - * @return True if it flees, false otherwise. - */ - private boolean fleeFromPredators(List nearbyEntities, double deltaTime) { - // Find the nearest predator: - Predicate condition = e -> e instanceof Predator p && p.getHungerController().canEat(animal); - Predator nearestPredator = (Predator) animal.movementController.getNearestEntity(nearbyEntities, condition); - - if (nearestPredator == null) return false; - - // If a predator is found, flee! - animal.movementController.fleeFromEntity(nearestPredator, deltaTime); - return true; - } } \ No newline at end of file diff --git a/src/entities/generic/AnimalBreedingController.java b/src/entities/generic/AnimalBreedingController.java index 503735c..d76c02a 100644 --- a/src/entities/generic/AnimalBreedingController.java +++ b/src/entities/generic/AnimalBreedingController.java @@ -9,7 +9,8 @@ import java.util.List; /** - * Handles the breeding logic for animals. + * Handles the breeding logic for animals. This class is responsible for breeding animals + * with other animals of the same species and opposite genders. * * @author Mehmet Kutay Bozkurt and Anas Ahmed * @version 1.0 @@ -40,11 +41,13 @@ public List breed(List nearbyEntities) { double mateLitterSize = mateEntity.genetics.getMaxLitterSize(); double animalLitterSize = animal.genetics.getMaxLitterSize(); + // Randomly select a litter size, capped by one of the parents' litter size. int litterSize = (int) (Math.random() * Math.min(animalLitterSize, mateLitterSize)) + 1; List offsprings = new ArrayList<>(); for (int i = 0; i < litterSize; i++) { AnimalGenetics childGenetics = animal.genetics.breed(mateEntity.genetics); + // Get a random position in a radius around the parent animal: Vector newPos = animal.position.getRandomPointInRadius(animal.genetics.getMaxOffspringSpawnDistance()); offsprings.add(animal.createOffspring(childGenetics, newPos)); } @@ -52,20 +55,21 @@ public List breed(List nearbyEntities) { } /** - * Checks if they have different genders and if the potential mate is alive and can multiply. * @param nearbyEntities The list of entities to search for a mate from. * @return A random mate from the list of entities, null if no mate found. */ private Animal getRandomMate(List nearbyEntities) { List potentialMates = nearbyEntities.stream() - .filter(this::canMateWith) + .filter(this::canMateWith) // Filter out entities that can't mate with this animal. .toList(); if (potentialMates.isEmpty()) return null; + // Return a random mate from the list of potential mates: return (Animal) potentialMates.get((int) (Math.random() * potentialMates.size())); } /** + * Checks if the animals have different genders, are of the same species, and can breed. * @return True if this animal can mate with the specified entity, false otherwise. */ protected boolean canMateWith(Entity entity) { @@ -81,13 +85,15 @@ protected boolean canMateWith(Entity entity) { } /** - * Animals can only breed if they have eaten food once in their life. - * TODO: Minimum food requirement to control insane population growth. + * Animals can only breed if they have eaten food once in their life, have enough food to breed, + * are not asleep and can multiply. + * @see Entity#canMultiply() * @return True if this animal can breed/multiply, false otherwise. */ private boolean canBreed() { return animal.canMultiply() && animal.hungerController.hasEaten() - && animal.hungerController.getFoodLevel() >= Data.getAnimalBreedingCost(); + && animal.hungerController.getFoodLevel() >= Data.getAnimalBreedingCost() + && !animal.isAsleep; } } \ No newline at end of file diff --git a/src/entities/generic/AnimalHungerController.java b/src/entities/generic/AnimalHungerController.java index 090061c..486be46 100644 --- a/src/entities/generic/AnimalHungerController.java +++ b/src/entities/generic/AnimalHungerController.java @@ -6,30 +6,36 @@ import java.util.List; /** - * Handles the hunger and food level logic for animals. + * Handles the hunger and food level logic for animals. Contains the food level + * of the animal, which is a double value between 0 and 1, and if the animal has + * eaten before. This is used to control breeding and to deplete the food level + * more. * * @author Mehmet Kutay Bozkurt and Anas Ahmed * @version 1.0 */ public class AnimalHungerController { - private final Animal animal; // The animal this controller is controlling - private boolean hasEaten = false; // Stores if the animal has eaten at least once or not -- used for breeding - protected double foodLevel; // The current food level of the animal (as a ratio between 0 and 1) + private final Animal animal; // The animal this controller is controlling. + protected double foodLevel; // The current food level of the animal (as a ratio between 0 and 1). + private boolean hasEaten = false; // Stores if the animal has eaten at least once or not -- used for breeding. + + /** + * Constructor. + * @param animal The animal to control the hunger for. + */ public AnimalHungerController(Animal animal) { this.animal = animal; - foodLevel = 0.4; // Spawn with 40% food + foodLevel = 0.4; // Spawn with 40% food. } /** * Attempts to eat any colliding entities. - * @param nearbyEntities The entities in the sight radius of this animal. + * @param nearbyEntities The entities that will be searched through -- are in the sight radius of this animal. */ public void eat(List nearbyEntities) { - if (nearbyEntities == null) return; - for (Entity entity : nearbyEntities) { - if (!(this.canEat(entity) && animal.isColliding(entity))) continue; + if (!this.canEat(entity) || !animal.isColliding(entity)) continue; double entitySizeRatio = (double) entity.getSize() / animal.getSize(); double foodValue = (entity instanceof Animal ? Data.getFoodValueForAnimals() : Data.getFoodValueForPlants()); @@ -51,15 +57,17 @@ public void eat(List nearbyEntities) { public void handleHunger(double deltaTime, int numberOfOffsprings) { // Decrease food level based on current distance travelled, which is proportional to speed. double distanceTraveled = animal.movementController.getDistanceTravelled(); - double hungerDrainPerTick = Data.getAnimalHungerDrain() * distanceTraveled * deltaTime; // * genetics.getSight(); // TODO: Sight affects hunger drain as balancing system + double hungerDrainPerTick = Data.getAnimalHungerDrain() * distanceTraveled * deltaTime; - foodLevel -= hungerDrainPerTick * (animal.isAsleep ? 0.5 : 1); // If sleeping, consume 50% less food - foodLevel -= numberOfOffsprings / (numberOfOffsprings + 1 / Data.getAnimalBreedingCost()); // Food cost for breeding + // If sleeping, consume 50% less food, if hasn't eaten yet, consume 125% more food to increase fragility of children. + foodLevel -= hungerDrainPerTick * (animal.isAsleep ? 0.5 : 1) * (hasEaten ? 1 : 2.25); + foodLevel -= numberOfOffsprings / (numberOfOffsprings + 1 / Data.getAnimalBreedingCost()); // Food cost for breeding. if (foodLevel <= 0) animal.setDead(); } /** - * Checks if this animal can eat a specified entity. + * Checks if this animal can eat a specified entity. Also checks if the given + * entity is alive or not. * @param entity The entity to check if this animal can eat. * @return True if this animal can eat the entity, false otherwise. */ @@ -68,10 +76,11 @@ public boolean canEat(Entity entity) { } /** + * The animal is considered to be hungry if it has also not eaten at least once. * @return True if the animal is hungry, false otherwise. */ public boolean isHungry() { - return foodLevel <= Data.getAnimalHungerThreshold(); + return foodLevel <= Data.getAnimalHungerThreshold() || !this.hasEaten; } /** diff --git a/src/entities/generic/AnimalMovementController.java b/src/entities/generic/AnimalMovementController.java index b26372e..9845aa6 100644 --- a/src/entities/generic/AnimalMovementController.java +++ b/src/entities/generic/AnimalMovementController.java @@ -7,19 +7,23 @@ import java.util.List; import java.util.function.Predicate; +import entities.Predator; + /** - * Handles all movement for an animal. + * Handles all movement for an animal. This includes wandering, moving to food, moving + * to mates, and fleeing from entities that can eat the animal. * * @author Mehmet Kutay Bozkurt and Anas Ahmed * @version 1.0 */ public class AnimalMovementController { - private final Animal animal; // The animal this controller controls - private Vector lastPosition; // The last position of the animal -- used to calculate speed - private double direction; // The direction the animal is moving in (in radians) + private final Animal animal; // The animal this controller controls. + + private double direction; // The direction the animal is moving in (in radians). + private Vector lastPosition; // The last position of the animal -- used to calculate speed. /** - * Constructor + * Constructor. * @param animal The animal to control movement for. * @param position The initial position of the animal. */ @@ -50,51 +54,6 @@ public void wander(Field field, double deltaTime) { animal.setPosition(newPosition); } - - /** - * Moves to or away from another entity. - * @param entity The entity to move to. - * @param deltaTime Delta time. - * @param moveAwayFromEntity If true, run away from entity instead of towards entity. - */ - private void moveRelativeToEntity(Entity entity, double deltaTime, boolean moveAwayFromEntity) { - double speed = animal.genetics.getMaxSpeed() * deltaTime; - Vector currentPos = animal.getPosition(); - - Vector difference; - if (moveAwayFromEntity) { - difference = currentPos.subtract(entity.position); - } else { - difference = entity.position.subtract(currentPos); - } - - // Do nothing if the entity is at the same position (avoids division by zero): - if (difference.getMagnitudeSquared() < Utility.EPSILON) return; - - Vector movement = difference.normalise().multiply(speed); - Vector newPosition = currentPos.add(movement); - animal.setPosition(newPosition); - } - - /** - * Move to an entity. - * @param entity The entity to move to. - * @param deltaTime Delta time. - */ - public void moveToEntity(Entity entity, double deltaTime) { - moveRelativeToEntity(entity, deltaTime, false); - } - - /** - * Flee from an entity. Move in the other direction from the entity. - * @param entity The entity to flee from. - * @param deltaTime Delta time. - */ - public void fleeFromEntity(Entity entity, double deltaTime) { - moveRelativeToEntity(entity, deltaTime, true); - } - - /** * Moves to the nearest entity that this animal can eat, returns false if there are none nearby. * @param entities The list of entities to search for food from. @@ -125,6 +84,24 @@ public boolean moveToNearestMate(List entities, double deltaTime) { return true; } + /** + * Searches for nearby predators and flees if found. Predator to this animal + * is another animal that can eat it. + * @param nearbyEntities The list of nearby entities to look through. + * @param deltaTime The delta time. + * @return True if it flees, false otherwise. + */ + public boolean fleeFromPredators(List nearbyEntities, double deltaTime) { + // Find the nearest predator using a predicate: + Predicate condition = e -> e instanceof Predator p && p.canEat(animal); + Predator nearestPredator = (Predator) getNearestEntity(nearbyEntities, condition); + if (nearestPredator == null) return false; + + // If a predator is found, flee! + fleeFromEntity(nearestPredator, deltaTime); + return true; + } + /** * Search a list of nearby entities and return the nearest entity satisfying the condition, or null. * @implNote If there are multiple entities at the same distance, the first one in the list is returned. @@ -132,7 +109,7 @@ public boolean moveToNearestMate(List entities, double deltaTime) { * @param condition The condition to determine what entities to move towards. * @return The nearest entity satisfying the condition, null if none found. */ - public Entity getNearestEntity(List entities, Predicate condition) { + private Entity getNearestEntity(List entities, Predicate condition) { Entity nearestEntity = null; double closestDistance = Double.MAX_VALUE; @@ -150,7 +127,50 @@ public Entity getNearestEntity(List entities, Predicate conditio } /** - * Set the last position of the animal. + * Moves to or away from another entity. + * @param entity The entity to move to. + * @param deltaTime Delta time. + * @param moveAwayFromEntity If true, run away from entity instead of towards entity. + */ + private void moveRelativeToEntity(Entity entity, double deltaTime, boolean moveAwayFromEntity) { + double speed = animal.genetics.getMaxSpeed() * deltaTime; + Vector currentPos = animal.getPosition(); + + Vector difference; + if (moveAwayFromEntity) { + difference = currentPos.subtract(entity.position); + } else { + difference = entity.position.subtract(currentPos); + } + + // Do nothing if the entity is at the same position (avoids division by zero): + if (difference.getMagnitudeSquared() < Utility.EPSILON) return; + + Vector movement = difference.normalise().multiply(speed); + Vector newPosition = currentPos.add(movement); + animal.setPosition(newPosition); + } + + /** + * Move to an entity. + * @param entity The entity to move to. + * @param deltaTime Delta time. + */ + private void moveToEntity(Entity entity, double deltaTime) { + moveRelativeToEntity(entity, deltaTime, false); + } + + /** + * Flee from an entity. Move in the other direction from the entity. + * @param entity The entity to flee from. + * @param deltaTime Delta time. + */ + private void fleeFromEntity(Entity entity, double deltaTime) { + moveRelativeToEntity(entity, deltaTime, true); + } + + /** + * Set the last position of the animal. Used for calculating speed. * @param lastPosition The last position of the animal. */ public void setLastPosition(Vector lastPosition) { @@ -158,7 +178,8 @@ public void setLastPosition(Vector lastPosition) { } /** - * @return The distance travelled in the current frame relative to the last + * @implNote Uses current position and the last position to calculate. + * @return The distance travelled in the current frame relative to the last. */ public double getDistanceTravelled() { return animal.getPosition().subtract(lastPosition).getMagnitude(); diff --git a/src/entities/generic/AnimalTest.java b/src/entities/generic/AnimalTest.java index 2ad5068..6b4a2ac 100644 --- a/src/entities/generic/AnimalTest.java +++ b/src/entities/generic/AnimalTest.java @@ -99,14 +99,6 @@ void testEat_EntityAlreadyDead() { assertFalse(prey.isAlive()); assertEquals(animal.hungerController.getFoodLevel(), initialFoodLevel); } - - @Test - void testEat_NullList() { - double initialFoodLevel = animal.hungerController.getFoodLevel(); - animal.hungerController.eat(null); - assertTrue(animal.isAlive()); - assertEquals(animal.hungerController.getFoodLevel(), initialFoodLevel); - } @Test void testMoveToNearestFood_NoNearbyEntities() { diff --git a/src/entities/generic/Entity.java b/src/entities/generic/Entity.java index 5f498b3..148786b 100644 --- a/src/entities/generic/Entity.java +++ b/src/entities/generic/Entity.java @@ -34,7 +34,7 @@ public Entity(Genetics genetics, Vector position) { * @param entity The entity to check collision with self. * @return True if colliding with entity (uses circle hit box), false otherwise or if entitiy is itself. */ - public boolean isColliding(Entity entity) { + protected boolean isColliding(Entity entity) { if (entity == null || entity == this) return false; double distanceSquared = this.position.subtract(entity.position).getMagnitudeSquared(); @@ -56,10 +56,12 @@ public List searchNearbyEntities(Field field, double searchRadius) { /** * Handles overcrowding of entities of the same species. * Looks at the genetics of the species to determine overcrowding. - * @param field the field that will be searched through for nearby entities + * @param nearbyEntities The entities that are nearby to search through. */ - public void handleOvercrowding(Field field) { - List entities = searchNearbyEntities(field, genetics.getOvercrowdingRadius()); + public void handleOvercrowding(List nearbyEntities) { + List entities = nearbyEntities.stream() + .filter(e -> position.subtract(e.getPosition()).getMagnitude() <= genetics.getOvercrowdingRadius()) + .toList(); List sameSpecies = getSameSpecies(entities); if (sameSpecies.size() >= genetics.getOvercrowdingThreshold()) { setDead(); @@ -153,6 +155,6 @@ public void setPosition(Vector position) { // Getters: public Vector getPosition() { return position; } public String getName() { return genetics.getName(); } - public int getSize() { return genetics.getSize(); } //This getter is for code simplicity + public int getSize() { return genetics.getSize(); } // This getter is for code simplicity. public boolean isAlive() { return isAlive; } } \ No newline at end of file diff --git a/src/entities/generic/EntityTest.java b/src/entities/generic/EntityTest.java index 7c67647..65bf4df 100644 --- a/src/entities/generic/EntityTest.java +++ b/src/entities/generic/EntityTest.java @@ -13,6 +13,12 @@ import simulation.simulationData.Data; import util.Vector; +/** + * Test class for the Entity class. Tests methods under the Entity class. + * + * @author Mehmet Kutay Bozkurt and Anas Ahmed + * @version 1.0 + */ class EntityTest { private Animal animal; private AnimalGenetics genetics; diff --git a/src/genetics/AnimalGenetics.java b/src/genetics/AnimalGenetics.java index e5c822c..fa0bc37 100644 --- a/src/genetics/AnimalGenetics.java +++ b/src/genetics/AnimalGenetics.java @@ -14,11 +14,11 @@ * @version 1.0 */ public class AnimalGenetics extends Genetics { - private final int maxLitterSize; // Maximum number of offspring per breeding - private final double maxSpeed; // Max speed of the entity - private final double sight; // Range at which the entity can see other entities - private final Gender gender; // The gender of the animal - private final String[] eats; // The names of the entities the animal eats + private final int maxLitterSize; // Maximum number of offspring per breeding. + private final double maxSpeed; // Max speed of the entity. + private final double sight; // Range at which the entity can see other entities. + private final Gender gender; // The gender of the animal. + private final String[] eats; // The names of the entities the animal eats. /** * Constructor -- Creates a new set of genetics for an animal. diff --git a/src/genetics/Genetics.java b/src/genetics/Genetics.java index 1c41f23..ec28fd7 100644 --- a/src/genetics/Genetics.java +++ b/src/genetics/Genetics.java @@ -10,16 +10,16 @@ * @version 1.0 */ public abstract class Genetics { - private final String name; // Name of entity, acts as identifying key - private final int maxAge; // Maximum age of the entity - private final int matureAge; // Age at which the entity can start breeding - private final double multiplyingRate; // Rate at which the entity multiplies -- breads (animal) or spreads (plant) - private final int size; // Size of the entity (the radius of the circle representing the entity) - private final Color colour; // RGB colour of the entity - private final int overcrowdingThreshold; // Number of entities at which the entity will die - private final double overcrowdingRadius; // Radius inside of which the threshold is checked - private final double maxOffspringSpawnDistance; // Maximum distance from the parent entity that the offspring can spawn - private final double mutationRate; // Rate at which the entity's genetics can mutate + private final String name; // Name of entity, acts as identifying key. + private final int maxAge; // Maximum age of the entity. + private final int matureAge; // Age at which the entity can start breeding. + private final double multiplyingRate; // Rate at which the entity multiplies -- breads (animal) or spreads (plant). + private final int size; // Size of the entity (the radius of the circle representing the entity). + private final Color colour; // RGB colour of the entity. + private final int overcrowdingThreshold; // Number of entities at which the entity will die. + private final double overcrowdingRadius; // Radius inside of which the threshold is checked. + private final double maxOffspringSpawnDistance; // Maximum distance from the parent entity that the offspring can spawn. + private final double mutationRate; // Rate at which the entity's genetics can mutate. /** * Constructor. diff --git a/src/genetics/mutation/AnimalMutator.java b/src/genetics/mutation/AnimalMutator.java index a188723..14b1cad 100644 --- a/src/genetics/mutation/AnimalMutator.java +++ b/src/genetics/mutation/AnimalMutator.java @@ -23,8 +23,12 @@ public class AnimalMutator extends Mutator { * @return The mutated genetics. */ public static AnimalGenetics mutateAnimalGenetics(AnimalGenetics genetics) { - List animalDatas = new ArrayList<>(Arrays.asList(Data.getPredatorsData())); + List animalDatas = new ArrayList<>(); + // Add all of the animal data to the list, without differentiating between predators and preys: + animalDatas.addAll(Arrays.asList(Data.getPredatorsData())); animalDatas.addAll(Arrays.asList(Data.getPreysData())); + + // Get the specific animal data for this species: AnimalData animalData = animalDatas .stream() .filter(ad -> ad.name.equals(genetics.getName())) @@ -32,7 +36,6 @@ public static AnimalGenetics mutateAnimalGenetics(AnimalGenetics genetics) { .orElse(null); if (animalData == null) return null; - double mutationRate = genetics.getMutationRate(); return new AnimalGenetics( diff --git a/src/genetics/mutation/Mutator.java b/src/genetics/mutation/Mutator.java index 850f197..ab02883 100644 --- a/src/genetics/mutation/Mutator.java +++ b/src/genetics/mutation/Mutator.java @@ -3,7 +3,8 @@ import simulation.simulationData.Data; /** - * Responsible for mutating genetics. + * Responsible for mutating genetics. Contains methods for mutating a single value + * in double and int. * * @author Mehmet Kutay Bozkurt and Anas Ahmed * @version 1.0 @@ -18,7 +19,8 @@ public class Mutator { */ protected static double singleMutate(double value, double[] interval, double mutationRate) { if (Math.random() >= mutationRate) return value; - double mutatedValue = value + value * Data.getMutationFactor() * (Math.random() > 0.5 ? 1 : -1); // Randomly increase or decrease the value. + // Randomly increase or decrease the value: + double mutatedValue = value + value * Data.getMutationFactor() * (Math.random() > 0.5 ? 1 : -1); return Math.max(interval[0], Math.min(interval[1], mutatedValue)); } diff --git a/src/genetics/mutation/PlantMutator.java b/src/genetics/mutation/PlantMutator.java index ee5bc62..bf6adda 100644 --- a/src/genetics/mutation/PlantMutator.java +++ b/src/genetics/mutation/PlantMutator.java @@ -21,13 +21,13 @@ public class PlantMutator extends Mutator { * @return The mutated genetics. */ public static PlantGenetics mutatePlantGenetics(PlantGenetics genetics) { + // Get the specific plant data for this species: PlantData plantData = Arrays.stream(Data.getPlantsData()) .filter(pd -> pd.name.equals(genetics.getName())) .findFirst() .orElse(null); if (plantData == null) return null; - double mutationRate = genetics.getMutationRate(); return new PlantGenetics( diff --git a/src/graphics/Display.java b/src/graphics/Display.java index d05c243..85b7aaf 100644 --- a/src/graphics/Display.java +++ b/src/graphics/Display.java @@ -107,7 +107,7 @@ public void drawArrow(int x1, int y1, double direction, double length, Color col int tipY = (int) (tip.y() + y1); double angleOffset = 1.5 * Math.PI / 2; - drawLine(x1, y1, direction, length, color); // Main line + drawLine(x1, y1, direction, length, color); // Main line. drawLine(tipX, tipY, direction + angleOffset, length / 3, color); drawLine(tipX, tipY, direction - angleOffset, length / 3, color); } diff --git a/src/graphics/RenderPanel.java b/src/graphics/RenderPanel.java index 8a30bab..be27b7b 100644 --- a/src/graphics/RenderPanel.java +++ b/src/graphics/RenderPanel.java @@ -42,11 +42,11 @@ public void drawRect(int x, int y, int width, int height, Color color, boolean f public void drawEqualTriangle(int centerX, int centerY, int radius, Color color) { data.add(new DrawEqualTriangle("drawEqualTriangle", centerX, centerY, radius, getArrayFromColor(color))); } - + public void drawText(String text, int fontSize, int x, int y, Color color) { data.add(new DrawText("drawText", text, fontSize, x, y, getArrayFromColor(color))); } - + public void drawLine(int x1, int y1, int x2, int y2, Color color) { data.add(new DrawLine("drawLine", x1, y1, x2, y2, getArrayFromColor(color))); } diff --git a/src/simulation/Field.java b/src/simulation/Field.java index 7cdbc1b..8568798 100644 --- a/src/simulation/Field.java +++ b/src/simulation/Field.java @@ -150,7 +150,7 @@ public void spawnNewEntities() { */ public void updateEnvironment() { if (Data.getDoDayNightCycle()) { - environment.incrementTime(Data.getDayNightCycleSpeed() * 0.01); + environment.updateTime(Data.getDayNightCycleSpeed() * 0.01); } if (Data.getDoWeatherCycle()) { diff --git a/src/simulation/FieldBuilder.java b/src/simulation/FieldBuilder.java index cb4cd5d..9b92034 100644 --- a/src/simulation/FieldBuilder.java +++ b/src/simulation/FieldBuilder.java @@ -20,10 +20,9 @@ public class FieldBuilder { private ArrayList entities; // The list of entities. /** - * Constructor. + * Constructor -- Create a FieldBuilder with the given width and height and create all entities. * @param width The width of the field. * @param height The height of the field. - * @param data The simulation data. */ public FieldBuilder(int width, int height) { this.width = width; diff --git a/src/simulation/environment/Environment.java b/src/simulation/environment/Environment.java index c7a177e..2a7084b 100644 --- a/src/simulation/environment/Environment.java +++ b/src/simulation/environment/Environment.java @@ -1,193 +1,87 @@ package simulation.environment; import graphics.Display; -import simulation.simulationData.Data; - -import java.awt.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import util.Vector; /** - * Stores the weather and the time of day. The weather can be one of the following, - * with the corresponding effects: - * CLEAR - No effect. - * RAINING - Plants multiply faster. - * WINDY - Random vector pushes animals around. - * STORM - Animals move slower + windy effects. + * Stores the weather and time controllers and delegates the updating of the + * environment to them. * * @author Anas Ahmed and Mehmet Kutay Bozkurt * @version 1.0 */ public class Environment { - // Daytime is 8 AM to 8 PM. - private int day = 1; // The current day - private int lastUpdateDay = 1; // The last day the weather was updated - private double timeOfDay = DAY_START; // Loops from 0 to 1. The current time of day - private double windDirection; // The direction of the wind in radians - private Weather weather; // The current weather - - private final static int PARTICLE_SPAWN_RATE = 4; // The number of rain particles to spawn per update - private final List rainParticles; // The rain particles on the screen - - private static final double LIGHTNING_SPAWN_PROBABILITY = 0.03; // The probability of spawning a lightning bolt - private final List lightnings; // The lightning bolts on the screen - - private final static double DAY_START = 1 / 3d; // Corresponds to 8am - private final static double DAY_END = 0.8d + 1 / 30d; // Corresponds to 8pm - + private WeatherController weatherController; // The weather controller. + private TimeController timeController; // The time controller. /** - * Constructor -- Creates a new environment with a random weather and wind direction. + * Constructor -- Creates a new environment with weather and time controllers. */ public Environment() { - setRandomWeather(); - windDirection = Math.random() * Math.PI * 2; - rainParticles = new ArrayList<>(); - lightnings = new ArrayList<>(); + weatherController = new WeatherController(); + timeController = new TimeController(); } /** - * Sets the current weather to be a random weather. The new weather cannot be the - * same as the current weather. + * Draws the weather effects on the display. + * @param display The display to update the weather effects on. */ - private void setRandomWeather() { - List weathers = new ArrayList<>(Arrays.asList(Weather.values())); - weathers.remove(weather); // Ensure that the new weather won't be the same one. - int randomIndex = (int) (Math.random() * weathers.size()); - weather = weathers.get(randomIndex); + public void drawWeatherEffects(Display display) { + weatherController.drawWeatherEffects(display); } /** - * Updates the wind direction. Updates the current weather when the day changes. + * Draws the weather that affects the simulation. */ public void updateWeather() { - double turbulence = weather == Weather.STORM ? 0.1 : 0.05; - windDirection += (Math.random() - 0.5) * Math.PI * turbulence; - - if (lastUpdateDay == day) return; - lastUpdateDay = day; - if (Math.random() < Data.getWeatherChangeProbability()) { - setRandomWeather(); - } - } - - /** - * Increments the time of day and the current day. - * @param dayNightCycleRate The amount of time to pass per update. - */ - public void incrementTime(double dayNightCycleRate) { - timeOfDay += dayNightCycleRate; - if (timeOfDay >= 1) { - timeOfDay = 0; - day++; - } - } - - /** - * Daytime is any time from 8 AM to 8 PM. - * @return True if the time is day, false otherwise. - */ - public boolean isDay() { - return timeOfDay <= DAY_END && timeOfDay >= DAY_START; - } - - /** - * Converts the time of day value from 0-1 to a 24-hour clock time. - * @return The time of day in the 24-hour format (HH:MM:SS) in a String. - */ - private String get24HourTime() { - double total_hours = timeOfDay * 24; - int hours = (int) total_hours; - int minutes = (int) ((total_hours - hours) * 60); - int seconds = (int) ((((total_hours - hours) * 60) - minutes) * 60); - return String.format("%02d:%02d:%02d", hours, minutes, seconds); + weatherController.updateWeather(); } /** - * @return The time with some additional information to be displayed to the screen. + * Draws the weather text onto the display. + * @param display The display to draw. */ - public String getTimeFormatted() { - String isDay = isDay() ? "Day time" : "Night time"; - return get24HourTime() + " | " + isDay + " | Day: " + day; + public void drawWeatherText(Display display) { + weatherController.drawWeatherText(display); } /** - * Updates the weather effects on the screen; that is, the lightning and rain effects. - * @param display The display to draw the effects onto. - */ - public void updateWeatherEffects(Display display) { - rainParticles.removeIf(p -> p.isOutOfBounds(display)); - for (RainParticle particle : rainParticles) { - particle.update(display, getWindVector()); - } - - lightnings.removeIf(Lightning::isDead); - for (Lightning lightning : lightnings) { - lightning.draw(display); - lightning.incrementAge(); - } - } - - /** - * Spawns rain particles on the top section of the screen. Some particles spawn out of - * bounds on the left and right side of the screen, so they will "seem" as coming from the sides. - * @param display The display to spawn the rain particles onto. - */ - public void spawnRain(Display display) { - if (weather == Weather.CLEAR || weather == Weather.WINDY) return; - - for (int i = 0; i < PARTICLE_SPAWN_RATE; i++) { - // Spawn the particles on the top section of the screen. - Vector spawnPosition = new Vector((Math.random() - 0.5) * display.getWidth() * 4, 1); - rainParticles.add(new RainParticle(spawnPosition)); - } - } - - /** - * Spawns a lightning bolt on the screen with a certain probability. - * Spawns lightning bolts only when there is a storm. - * @param display The display to draw the lightning onto. + * Updates the time of day, and if the day has changed, change the weather. + * @param dayNightCycleRate The amount of time to pass per update. */ - public void spawnLightning(Display display) { - if (weather != Weather.STORM) return; - - if (Math.random() < LIGHTNING_SPAWN_PROBABILITY) { - lightnings.add(new Lightning(display)); + public void updateTime(double dayNightCycleRate) { + boolean dayChanged = timeController.incrementTime(dayNightCycleRate); + if (dayChanged) { + weatherController.changeWeather(); } } /** - * Draws the screen effects -- the darkness effect. - * @param display The display to draw the effects onto. + * Draws the darkness effect onto the display. + * @param display The display to draw the effects on. */ - public void drawScreenEffects(Display display) { - drawDarknessEffects(display); + public void drawDarknessEffect(Display display) { + timeController.drawDarknessEffect(display); } /** - * Draws a transparent black rect, opacity depending on time of night. - * @param display The display to draw the effect onto. + * Draws the time effects on the display; mainly time text. + * @param display The display to update the time effects on. */ - private void drawDarknessEffects(Display display) { - double lightLevel = Math.min(timeOfDay, 1 - timeOfDay); - - lightLevel *= 2; - lightLevel = Math.min(lightLevel, 0.8) + 0.2; // Small period of time of full lightness - - double alpha = 0.6 * (1 - lightLevel); - display.drawTransparentRectangle(0, 0, display.getWidth(), display.getHeight(), alpha, Color.black); + public void drawTimeText(Display display) { + timeController.drawTimeText(display); } /** - * @return The wind vector using the wind direction. + * @return The time with some additional information to be displayed to the screen. */ - public Vector getWindVector() { - return Vector.getVectorFromAngle(windDirection); + public String getTimeFormatted() { + return timeController.getTimeFormatted(); } // Getters: - public Weather getWeather() { return weather; } - public double getWindDirection() { return windDirection; } + public Weather getWeather() { return weatherController.getWeather(); } + public double getWindDirection() { return weatherController.getWindDirection(); } + public Vector getWindVector() { return weatherController.getWindVector(); } + public boolean isDay() { return timeController.isDay(); } } \ No newline at end of file diff --git a/src/simulation/environment/RainParticle.java b/src/simulation/environment/RainParticle.java index 98ad19f..c902bf8 100644 --- a/src/simulation/environment/RainParticle.java +++ b/src/simulation/environment/RainParticle.java @@ -15,9 +15,9 @@ * @version 1.0 */ public class RainParticle { - private Vector position; // The current position of the rain particle + private Vector position; // The current position of the rain particle. private Vector lastPosition; // The last position of the rain partcile. - private final Color color; // The color of the rain particle + private final Color color; // The color of the rain particle. private static final Vector GRAVITY_VECTOR = new Vector(0, 5); // The gravity that affects a rain particle. @@ -65,7 +65,7 @@ private void draw(Display display) { /** * @implNote Only the bottom side of the display is checked if the rain particle is out of bounds. * This is because the rain particles can be drawn from the sides of the display. - * @see Environment.spawnRain + * @see WeatherController#spawnRain() * @param display The display to check if the rain particle is out of bounds of. * @return True if the rain particle is out of bounds of the display, false otherwise. */ diff --git a/src/simulation/environment/TimeController.java b/src/simulation/environment/TimeController.java new file mode 100644 index 0000000..eb6dcde --- /dev/null +++ b/src/simulation/environment/TimeController.java @@ -0,0 +1,85 @@ +package simulation.environment; + +import java.awt.Color; + +import graphics.Display; + +/** + * Stores the weather and the time of day and updates it accordingly. + * + * @author Anas Ahmed and Mehmet Kutay Bozkurt + * @version 1.0 + */ +public class TimeController { + private int day = 1; // The current day + private double timeOfDay = DAY_START; // Loops from 0 to 1. The current time of day. + + // Daytime is 8 AM to 8 PM: + private final static double DAY_START = 1 / 3d; // Corresponds to 8 AM. + private final static double DAY_END = 0.8d + 1 / 30d; // Corresponds to 8 PM. + + /** + * Increments the time of day and the current day. + * @param dayNightCycleRate The amount of time to pass per update. + * @return True if the day has changed, false otherwise. + */ + public boolean incrementTime(double dayNightCycleRate) { + timeOfDay += dayNightCycleRate; + if (timeOfDay >= 1) { + timeOfDay = 0; + day++; + return true; + } + return false; + } + + /** + * Daytime is any time from 8 AM to 8 PM. + * @return True if the time is day, false otherwise. + */ + public boolean isDay() { + return timeOfDay <= DAY_END && timeOfDay >= DAY_START; + } + + /** + * Converts the time of day value from 0-1 to a 24-hour clock time. + * @return The time of day in the 24-hour format (HH:MM:SS) in a String. + */ + private String get24HourTime() { + double total_hours = timeOfDay * 24; + int hours = (int) total_hours; + int minutes = (int) ((total_hours - hours) * 60); + int seconds = (int) ((((total_hours - hours) * 60) - minutes) * 60); + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } + + /** + * @return The time with some additional information to be displayed to the screen. + */ + public String getTimeFormatted() { + String isDay = isDay() ? "Day time" : "Night time"; + return get24HourTime() + " | " + isDay + " | Day: " + day; + } + + /** + * Renders text of the current time of day. + */ + public void drawTimeText(Display display) { + String time = getTimeFormatted(); + display.drawText(time, 20, 5, 20, Color.WHITE); + } + + /** + * Draws a transparent black rect, opacity depending on time of night. + * @param display The display to draw the effect onto. + */ + public void drawDarknessEffect(Display display) { + double lightLevel = Math.min(timeOfDay, 1 - timeOfDay); + + lightLevel *= 2; + lightLevel = Math.min(lightLevel, 0.8) + 0.2; // Small period of time of full lightness. + + double alpha = 0.6 * (1 - lightLevel); + display.drawTransparentRectangle(0, 0, display.getWidth(), display.getHeight(), alpha, Color.BLACK); + } +} diff --git a/src/simulation/environment/WeatherController.java b/src/simulation/environment/WeatherController.java new file mode 100644 index 0000000..369353f --- /dev/null +++ b/src/simulation/environment/WeatherController.java @@ -0,0 +1,141 @@ +package simulation.environment; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import graphics.Display; +import simulation.simulationData.Data; +import util.Vector; + +/** + * Stores the weather and updates it accordingle. + * The weather can be one of the following, with the corresponding effects: + * CLEAR - No effect. + * RAINING - Plants multiply faster. + * WINDY - Random vector pushes animals around. + * STORM - Animals move slower, plus windy effects. + * + * @author Mehmet Kutay Bozkurt and Anas Ahmed + * @version 1.0 + */ +public class WeatherController { + private double windDirection; // The direction of the wind in radians. + private Weather weather; // The current weather. + + private final static int PARTICLE_SPAWN_RATE = 4; // The number of rain particles to spawn per update. + private final List rainParticles; // The rain particles on the screen. + + private static final double LIGHTNING_SPAWN_PROBABILITY = 0.03; // The probability of spawning a lightning bolt. + private final List lightnings; // The lightning bolts on the screen. + + /** + * Constructor -- Creates a new weather controller with a random weather and wind direction. + */ + public WeatherController() { + setRandomWeather(); + windDirection = Math.random() * Math.PI * 2; + rainParticles = new ArrayList<>(); + lightnings = new ArrayList<>(); + } + + /** + * Sets the current weather to be a random weather. The new weather cannot be the + * same as the current weather. + */ + private void setRandomWeather() { + List weathers = new ArrayList<>(Arrays.asList(Weather.values())); + weathers.remove(weather); // Ensure that the new weather won't be the same one. + int randomIndex = (int) (Math.random() * weathers.size()); + weather = weathers.get(randomIndex); + } + + /** + * Updates the wind direction. + */ + public void updateWeather() { + double turbulence = weather == Weather.STORM ? 0.1 : 0.05; + windDirection += (Math.random() - 0.5) * Math.PI * turbulence; + } + + /** + * Changes the weather randomly. + */ + public void changeWeather() { + if (Math.random() < Data.getWeatherChangeProbability()) { + setRandomWeather(); + } + } + + /** + * Draws the weather effects on the screen; that is, the lightning and rain effects. + * Also spawns the lightning bolts and the rain. + * @param display The display to draw the effects onto. + */ + public void drawWeatherEffects(Display display) { + spawnRain(display); + spawnLightning(display); + + rainParticles.removeIf(p -> p.isOutOfBounds(display)); + for (RainParticle particle : rainParticles) { + particle.update(display, getWindVector()); + } + + lightnings.removeIf(Lightning::isDead); + for (Lightning lightning : lightnings) { + lightning.draw(display); + lightning.incrementAge(); + } + } + + /** + * Draws the weather text, specifying the current weather and wind direction. + */ + public void drawWeatherText(Display display) { + display.drawText(weather.toString(), 20, 5, 40, Color.WHITE); + if (weather == Weather.WINDY || weather == Weather.STORM) { + display.drawText("Wind Direction:", 20, 5, 60, Color.WHITE); + display.drawCircle(180, 55, 20, Color.BLACK); + display.drawArrow(180, 55, windDirection, 20, Color.WHITE); + } + } + + /** + * Spawns rain particles on the top section of the screen. Some particles spawn out of + * bounds on the left and right side of the screen, so they will "seem" as coming from the sides. + * @param display The display to spawn the rain particles onto. + */ + private void spawnRain(Display display) { + if (weather == Weather.CLEAR || weather == Weather.WINDY) return; + + for (int i = 0; i < PARTICLE_SPAWN_RATE; i++) { + // Spawn the particles on the top section of the screen. + Vector spawnPosition = new Vector((Math.random() - 0.5) * display.getWidth() * 4, 1); + rainParticles.add(new RainParticle(spawnPosition)); + } + } + + /** + * Spawns a lightning bolt on the screen with a certain probability. + * Spawns lightning bolts only when there is a storm. + * @param display The display to draw the lightning onto. + */ + private void spawnLightning(Display display) { + if (weather != Weather.STORM) return; + + if (Math.random() < LIGHTNING_SPAWN_PROBABILITY) { + lightnings.add(new Lightning(display)); + } + } + + /** + * @return The wind vector using the wind direction. + */ + public Vector getWindVector() { + return Vector.getVectorFromAngle(windDirection); + } + + public Weather getWeather() { return weather; } + public double getWindDirection() { return windDirection; } +} diff --git a/src/simulation/quadTree/QuadTree.java b/src/simulation/quadTree/QuadTree.java index f0b8e62..706d17a 100644 --- a/src/simulation/quadTree/QuadTree.java +++ b/src/simulation/quadTree/QuadTree.java @@ -2,6 +2,7 @@ import java.awt.Color; import java.util.ArrayList; +import java.util.List; import entities.generic.Entity; import graphics.Display; @@ -95,8 +96,8 @@ public void insert(Entity entity) { * @param queryRange The range to query the quad tree. * @return A list of entities found in the given range. */ - public ArrayList query(Circle queryRange) { - ArrayList foundEntities = new ArrayList<>(); + public List query(Circle queryRange) { + List foundEntities = new ArrayList<>(); queryInternal(queryRange, foundEntities); return foundEntities; } @@ -107,7 +108,7 @@ public ArrayList query(Circle queryRange) { * @param queryRange The range to query the quad tree. * @param foundEntities The list to add the found entities to. */ - private void queryInternal(Circle queryRange, ArrayList foundEntities) { + private void queryInternal(Circle queryRange, List foundEntities) { if (!rect.intersects(queryRange)) return; // Check entities stored in this node @@ -117,8 +118,8 @@ private void queryInternal(Circle queryRange, ArrayList foundEntities) { } } - // Recursively query children if (!hasSubdivided) return; + // Recursively query children: topLeftTree.queryInternal(queryRange, foundEntities); topRightTree.queryInternal(queryRange, foundEntities); bottomLeftTree.queryInternal(queryRange, foundEntities); diff --git a/src/simulation/quadTree/Rectangle.java b/src/simulation/quadTree/Rectangle.java index cc35ede..8ca8e45 100644 --- a/src/simulation/quadTree/Rectangle.java +++ b/src/simulation/quadTree/Rectangle.java @@ -10,8 +10,8 @@ */ public record Rectangle(double x, double y, double w, double h) { /** - * @param point The point to check inside the rectangle - * @return True if the point is in the rectangle, false otherwise + * @param point The point to check inside the rectangle. + * @return True if the point is in the rectangle, false otherwise. */ public boolean hasPoint(Vector point) { double px = point.x(); diff --git a/src/simulation/simulationData/AnimalData.java b/src/simulation/simulationData/AnimalData.java index 8a90634..be4cd9c 100644 --- a/src/simulation/simulationData/AnimalData.java +++ b/src/simulation/simulationData/AnimalData.java @@ -14,18 +14,16 @@ * @version 1.0 */ public class AnimalData extends EntityData{ - public int[] maxLitterSize; // Maximum number of offspring per breeding - public double[] maxSpeed; // Speed of the entity - public double[] sight; // Range at which the entity can see other entities - public String[] eats; // List of entities that this entity can eat - public double[] maxFoodLevel; // Maximum food level of the entity - + public int[] maxLitterSize; // Maximum number of offspring per breeding. + public double[] maxSpeed; // Speed of the entity. + public double[] sight; // Range at which the entity can see other entities. + public String[] eats; // List of entities that this entity can eat. /** * @return A random set of genetics for an animal based on the data provided. */ public AnimalGenetics generateRandomGenetics() { - Color convertedColour = new Color(this.colour[0], this.colour[1], this.colour[2]); // Convert RGB data to java.swing.Color - Color mutatedColour = Utility.mutateColor(convertedColour, 1); // Change the colour slightly + Color convertedColour = new Color(this.colour[0], this.colour[1], this.colour[2]); // Convert RGB data to java.awt.Color. + Color mutatedColour = Utility.mutateColor(convertedColour, 1); // Change the colour slightly. return new AnimalGenetics( generateRandomNumberBetween(multiplyingRate[0], multiplyingRate[1]), diff --git a/src/simulation/simulationData/Data.java b/src/simulation/simulationData/Data.java index cd4e5c7..40eeb0b 100644 --- a/src/simulation/simulationData/Data.java +++ b/src/simulation/simulationData/Data.java @@ -9,9 +9,9 @@ * @version 1.0 */ public class Data { - public static final String PATH = System.getProperty("user.dir"); // The main directory of the project + public static final String PATH = System.getProperty("user.dir"); // The main directory of the project. - // The data of the simulation + // The data of the simulation: private static final SimulationData simulationData = Parser.parseSimulationData(Parser.getContentsOfFile(PATH + "/src/simulation_data.json")); // Getters: diff --git a/src/simulation/simulationData/EntityData.java b/src/simulation/simulationData/EntityData.java index a8d5e96..b35dffb 100644 --- a/src/simulation/simulationData/EntityData.java +++ b/src/simulation/simulationData/EntityData.java @@ -8,17 +8,17 @@ * @version 1.0 */ public abstract class EntityData { - public String name; // Name of the entity - public int numberOfEntitiesAtStart; // Number of entities at the start of the simulation - public int[] maxAge; // Maximum age of the entity - public int[] matureAge; // Age at which the entity can start breeding - public double[] multiplyingRate; // Rate at which the entity can multiply - public int[] size; // Size of the entity - public int[] colour; // RGB colour of the entity - public int[] overcrowdingThreshold; // Number of entities at which the entity will die - public double[] overcrowdingRadius; // Radius inside of which the threshold is checked - public double[] maxOffspringSpawnDistance; // Maximum distance offspring can spawn from the parent entity - public double[] mutationRate; // The rate at which the genetics will mutate + public String name; // Name of the entity. + public int numberOfEntitiesAtStart; // Number of entities at the start of the simulation. + public int[] maxAge; // Maximum age of the entity. + public int[] matureAge; // Age at which the entity can start breeding. + public double[] multiplyingRate; // Rate at which the entity can multiply. + public int[] size; // Size of the entity. + public int[] colour; // RGB colour of the entity. + public int[] overcrowdingThreshold; // Number of entities at which the entity will die. + public double[] overcrowdingRadius; // Radius inside of which the threshold is checked. + public double[] maxOffspringSpawnDistance; // Maximum distance offspring can spawn from the parent entity. + public double[] mutationRate; // The rate at which the genetics will mutate. /** * @return A random number between the given double min and max values. diff --git a/src/simulation/simulationData/PlantData.java b/src/simulation/simulationData/PlantData.java index 20bfac0..d449604 100644 --- a/src/simulation/simulationData/PlantData.java +++ b/src/simulation/simulationData/PlantData.java @@ -13,15 +13,15 @@ * @version 1.0 */ public class PlantData extends EntityData { - public int[] numberOfSeeds; // Number of seeds produced by the plant -- when multiplied - public double rainingGrowthFactor; // Growth factor when raining (affects number of seeds and range of growth) + public int[] numberOfSeeds; // Number of seeds produced by the plant -- when multiplied. + public double rainingGrowthFactor; // Growth factor when raining (affects number of seeds and range of growth). /** * @return A random set of genetics for a plant based on the data provided. */ public PlantGenetics generateRandomGenetics() { - Color convertedColour = new Color(this.colour[0], this.colour[1], this.colour[2]); // Convert rgRGB data to java.swing.Color - Color mutatedColour = Utility.mutateColor(convertedColour, 1); // Change the colour slightly + Color convertedColour = new Color(this.colour[0], this.colour[1], this.colour[2]); // Convert array RGB data to java.awt.Color. + Color mutatedColour = Utility.mutateColor(convertedColour, 1); // Change the colour slightly. return new PlantGenetics( generateRandomNumberBetween(maxAge[0], maxAge[1]), diff --git a/src/simulation/simulationData/SimulationData.java b/src/simulation/simulationData/SimulationData.java index 4f1257a..9371d48 100644 --- a/src/simulation/simulationData/SimulationData.java +++ b/src/simulation/simulationData/SimulationData.java @@ -1,25 +1,25 @@ package simulation.simulationData; public class SimulationData { - public AnimalData[] preysData; // An array of prey species data - public AnimalData[] predatorsData; // An array of predator species data - public PlantData[] plantsData; // An array of plant types data + public AnimalData[] preysData; // An array of prey species data. + public AnimalData[] predatorsData; // An array of predator species data. + public PlantData[] plantsData; // An array of plant types data. - public double foodValueForAnimals; // Scales the food value of animals - public double foodValueForPlants; // Scales the food value of plants - public double animalHungerDrain; // Controls rate of foodLevel depletion over time - public double animalBreedingCost; // Scales how much food is consumed on breeding; use 0 for no food cost + public double foodValueForAnimals; // Scales the food value of animals. + public double foodValueForPlants; // Scales the food value of plants. + public double animalHungerDrain; // Controls rate of foodLevel depletion over time. + public double animalBreedingCost; // Scales how much food is consumed on breeding; use 0 for no food cost. - public double mutationFactor; // The ratio that the genetics will mutate by - public double entityAgeRate; // Controls how fast the entities age - public double fieldScaleFactor; - public boolean doDayNightCycle; - public boolean doWeatherCycle; - public double weatherChangeProbability; - public double windStrength; - public double stormMovementSpeedFactor; - public boolean showQuadTrees; - public double dayNightCycleSpeed; + public double mutationFactor; // The ratio that the genetics will mutate by. + public double entityAgeRate; // Controls how fast the entities age. + public double fieldScaleFactor; // The size of the field, smaller value means more zoomed in. + public double weatherChangeProbability; // The probability of the weather changing. + public double windStrength; // The strength of the wind in windy conditions. + public double stormMovementSpeedFactor; // When a storm happens, the factor that hinders the entities' speed. + public double dayNightCycleSpeed; // The speed of the day-night cycle -- how fast the time passes. + public boolean doDayNightCycle; // Whether the day-night cycle is enabled. + public boolean doWeatherCycle; // Whether the weather cycle is enabled. + public boolean showQuadTrees; // Whether the quad trees are shown. public double animalHungerThreshold; // Animals will look for food at this threshold. public double animalDyingOfHungerThreshold; // Animals will prioritise looking for food at this threshold. diff --git a/src/simulation_data.json b/src/simulation_data.json index 8d9c134..996e417 100644 --- a/src/simulation_data.json +++ b/src/simulation_data.json @@ -1,31 +1,31 @@ { - "foodValueForAnimals": 0.8, + "foodValueForAnimals": 1, "foodValueForPlants": 0.1, - "animalHungerDrain": 0.0035, - "animalBreedingCost": 0.5, + "animalHungerDrain": 0.0045, + "animalBreedingCost": 0.2, "mutationFactor": 0.2, "entityAgeRate": 0.5, - "fieldScaleFactor" : 0.5, + "fieldScaleFactor" : 0.55, "doDayNightCycle" : true, "dayNightCycleSpeed" : 0.1, "doWeatherCycle" : true, "weatherChangeProbability" : 1, - "windStrength": 0.15, - "stormMovementSpeedFactor": 0.4, + "windStrength": 0.1, + "stormMovementSpeedFactor": 0.5, "showQuadTrees" : false, "animalHungerThreshold": 0.5, - "animalDyingOfHungerThreshold": 0.2, + "animalDyingOfHungerThreshold": 0.1, "predatorsData": [ { "name": "Fox", - "multiplyingRate": [0.05, 0.15], - "maxLitterSize": [1, 4], - "maxAge": [80, 120], - "matureAge": [40, 40], + "multiplyingRate": [0.01, 0.15], + "maxLitterSize": [1, 3], + "maxAge": [120, 180], + "matureAge": [60, 80], "mutationRate": [0.01, 0.05], "maxSpeed": [4, 6.5], - "sight": [30, 50], - "numberOfEntitiesAtStart": 12, + "sight": [30, 45], + "numberOfEntitiesAtStart": 8, "eats": ["Rabbit"], "size": [3, 6], "colour": [230, 20, 40], @@ -35,13 +35,13 @@ }, { "name": "Wolf", - "multiplyingRate": [0.05, 0.15], - "maxLitterSize": [2, 3], - "maxAge": [90, 120], - "matureAge": [50, 55], + "multiplyingRate": [0.01, 0.1], + "maxLitterSize": [1, 3], + "maxAge": [160, 220], + "matureAge": [70, 100], "mutationRate": [0.01, 0.05], "maxSpeed": [6, 7], - "sight": [35, 50], + "sight": [25, 50], "numberOfEntitiesAtStart": 8, "eats": ["Rabbit", "Squirrel"], "size": [4, 7], @@ -52,16 +52,16 @@ }, { "name": "Bear", - "multiplyingRate": [0.05, 0.15], + "multiplyingRate": [0.03, 0.05], "maxLitterSize": [2, 3], "maxAge": [160, 200], "matureAge": [100, 120], "mutationRate": [0.01, 0.05], - "maxSpeed": [5, 6.5], - "sight": [35, 50], + "maxSpeed": [3,4.5], + "sight": [55, 80], "numberOfEntitiesAtStart": 4, - "eats": ["Wolf"], - "size": [7, 12], + "eats": ["Wolf", "Fox"], + "size": [6, 9], "colour": [74, 25, 25], "overcrowdingThreshold": [8, 25], "overcrowdingRadius": [10, 15], @@ -71,12 +71,12 @@ "preysData": [ { "name": "Rabbit", - "multiplyingRate": [0.15, 0.2], - "maxLitterSize": [2, 6], + "multiplyingRate": [0.3, 0.5], + "maxLitterSize": [4, 8], "maxAge": [65, 100], - "matureAge": [30, 50], + "matureAge": [20, 30], "mutationRate": [0.01, 0.05], - "maxSpeed": [4, 6], + "maxSpeed": [3.5, 4.5], "sight": [25, 35], "numberOfEntitiesAtStart": 25, "eats": ["Grass"], @@ -88,14 +88,14 @@ }, { "name": "Squirrel", - "multiplyingRate": [0.2, 0.5], + "multiplyingRate": [0.1, 0.4], "maxLitterSize": [2, 5], "maxAge": [85, 105], "matureAge": [50, 60], "mutationRate": [0.01, 0.05], - "maxSpeed": [5, 7], + "maxSpeed": [3.5, 5], "sight": [20, 40], - "numberOfEntitiesAtStart": 12, + "numberOfEntitiesAtStart": 16, "eats": ["Grass"], "size": [3, 6], "colour": [40, 20, 200], @@ -113,12 +113,12 @@ "maxAge": [40, 60], "matureAge": [30, 40], "numberOfEntitiesAtStart": 200, - "size": [3, 6], + "size": [3, 7], "colour": [40, 240, 30], - "maxOffspringSpawnDistance": [9, 20], - "overcrowdingThreshold": [6, 9], - "overcrowdingRadius": [6, 20], - "rainingGrowthFactor" : 3 + "maxOffspringSpawnDistance": [2, 35], + "overcrowdingThreshold": [2, 8], + "overcrowdingRadius": [4, 25], + "rainingGrowthFactor" : 4 } ] } \ No newline at end of file diff --git a/src/util/Utility.java b/src/util/Utility.java index 20b3984..62fc87b 100644 --- a/src/util/Utility.java +++ b/src/util/Utility.java @@ -2,8 +2,7 @@ import simulation.simulationData.Data; -import java.awt.*; -import java.util.Random; +import java.awt.Color; /** * Utility class for various mathematical operations, constants, and helpers. @@ -13,30 +12,29 @@ */ public class Utility { public static final double EPSILON = 1e-4; // Epsilon value for floating point comparisons - private static final Random rand = new Random(); /** - * Linear interpolation + * Linear interpolation. */ public static double lerp(double a, double b, double t) { return a + t * (b - a); } /** - * Adds a random change in value to a colour - * Uses pre-defined mutation factor from data - * @param color the colour to mutate - * @return the mutated colour + * Adds a random change in value to a colour. + * Uses pre-defined mutation factor from the simulation data. + * @param color The colour to mutate. + * @return The mutated colour. */ public static Color mutateColor(Color color, double mutationRate) { return Utility.mutateColor(color, mutationRate, Data.getMutationFactor()); } /** - * Adds a random change in value to a colour - * @param color the colour to mutate - * @param mutationFactor how drastic the mutation is - * @return the mutated colour + * Adds a random change in value to a colour. + * @param color The colour to mutate. + * @param mutationFactor How drastic the mutation is. + * @return The mutated colour. */ public static Color mutateColor(Color color, double mutationRate, double mutationFactor) { if (mutationFactor < 0 || mutationFactor > 1) { @@ -51,15 +49,15 @@ public static Color mutateColor(Color color, double mutationRate, double mutatio } /** - * Adjusts a single RGB value randomly - * @param value the RGB value to mutate - * @param mutationFactor how drastic the mutation is - * @return the mutated value + * Adjusts a single RGB value randomly. + * @param value The RGB value to mutate. + * @param mutationFactor How drastic the mutation is. + * @return The mutated value. */ private static int mutateChannel(int value, double mutationRate, double mutationFactor) { - if (Math.random() >= mutationRate) return value; // No mutation - int mutation = (int) (value * mutationFactor * (rand.nextDouble() > 0.5 ? 1 : -1)); - return Math.max(0, Math.min(255, value + mutation)); // Clamp between 0 and 255 + if (Math.random() >= mutationRate) return value; // No mutation. + int mutation = (int) (value * mutationFactor * (Math.random() > 0.5 ? 1 : -1)); + return Math.max(0, Math.min(255, value + mutation)); // Clamp between 0 and 255. } /** diff --git a/src/view/Clock.java b/src/view/Clock.java index 240e0f7..58364aa 100644 --- a/src/view/Clock.java +++ b/src/view/Clock.java @@ -7,9 +7,9 @@ * @version 1.0 */ public class Clock { - private final double fps; - private double lastTick; // Time in nanoseconds - private double deltaTime; + private final double fps; // Frames per second to run the simulation at. + private double lastTick; // To calculate the delta time. + private double deltaTime; // Time between simulation steps in nanoseconds. /** * Constructor for the Clock class to handle the delay between simulation steps. @@ -24,7 +24,7 @@ public Clock(int fps) { * Waits a clock cycle (1 / fps) by waiting in a while loop. */ public void tick() { - while (System.nanoTime() < lastTick + (double) 1_000_000_000 / fps) { } + while (System.nanoTime() < lastTick + (double) 1_000_000_000 / fps); deltaTime = System.nanoTime() - lastTick; lastTick = System.nanoTime(); } diff --git a/src/view/Engine.java b/src/view/Engine.java index 2094d7d..e534a63 100644 --- a/src/view/Engine.java +++ b/src/view/Engine.java @@ -2,9 +2,7 @@ import entities.generic.Entity; import graphics.Display; -import simulation.environment.Environment; import simulation.Simulator; -import simulation.environment.Weather; import simulation.simulationData.Data; import java.awt.*; @@ -13,23 +11,24 @@ /** * Combines the Simulator and Display to visualise the simulation. - * This is the "engine" that runs the entire simulation. + * This is the "engine" that runs the entire simulation. Holds the Clock + * object to keep track of time. * * @author Anas Ahmed and Mehmet Kutay Bozkurt * @version 1.0 */ public class Engine { - private final Display display; // The GUI display - private final Simulator simulator; // The simulation - private final Clock clock; // Clock to keep track of time - private boolean running = false; // Whether the simulation is running + private final Display display; // The GUI display. + private final Simulator simulator; // The simulation. + private final Clock clock; // Clock to keep track of time. + private boolean running = false; // Whether the simulation is running. /** - * 0 < scaleFactor < 1 => field is zoomed in - * scaleFactor = 1 => field is screen size (1 field unit = 1px) - * scale factor > 1 => field is zoomed out + * 0 < scaleFactor < 1 => field is zoomed in. + * scaleFactor = 1 => field is screen size (1 field unit = 1px). + * scale factor > 1 => field is zoomed out. */ - private final double fieldScaleFactor; // Scales the field size up/down, so field size doesn't have to be screen size + private final double fieldScaleFactor; // Scales the field size up/down, so field size doesn't have to be screen size. /** * Constructor - Create an engine to run the simulation. @@ -53,7 +52,6 @@ public Engine(int displayWidth, int displayHeight, int fps) { private void run() { while (running) { simulator.step(); - display.fill(Color.BLACK); List entities = simulator.getField().getAllEntities(); @@ -63,63 +61,38 @@ private void run() { entity.draw(display, fieldScaleFactor); } + // Draw the weather effects. + if (Data.getDoWeatherCycle()) { + simulator.getField().environment.drawWeatherEffects(display); + } + + // Draw the darkning screen effect before any text to not obscure them: + if (Data.getDoDayNightCycle()) { + simulator.getField().environment.drawDarknessEffect(display); + } // Debug tool to show the quadtree. It also looks really cool! if (Data.getShowQuadTrees()) { simulator.getField().getQuadtree().draw(display, fieldScaleFactor); } - // Draw and update weather effects. + // Draw the weather text. if (Data.getDoWeatherCycle()) { - updateWeatherEffects(display); - drawWeatherText(); + simulator.getField().environment.drawWeatherText(display); } - drawFieldDataText(); - + // Draw the time text. if (Data.getDoDayNightCycle()) { - drawTimeText(); + simulator.getField().environment.drawTimeText(display); } + drawFieldDataText(); + display.update(); clock.tick(); } } - /** - * Update the weather effects on the display. - * @param display The display to update the weather effects on. - */ - private void updateWeatherEffects(Display display) { - Environment environment = simulator.getField().environment; - environment.spawnRain(display); - environment.drawScreenEffects(display); - environment.spawnLightning(display); - environment.updateWeatherEffects(display); - } - - /** - * Draws the weather text, specifying the current weather and wind direction. - */ - private void drawWeatherText() { - Weather weather = simulator.getField().environment.getWeather(); - display.drawText(weather.toString(), 20, 5, 40, Color.WHITE); - if (weather == Weather.WINDY || weather == Weather.STORM) { - display.drawText("Wind Direction:", 20, 5, 60, Color.WHITE); - double windDirection = simulator.getField().environment.getWindDirection(); - display.drawCircle(180, 55, 20, Color.BLACK); - display.drawArrow(180, 55, windDirection, 20, Color.WHITE); - } - } - - /** - * Renders text of the current time of day. - */ - private void drawTimeText() { - String time = simulator.getField().environment.getTimeFormatted(); - display.drawText(time, 20, 5, 20, Color.WHITE); - } - /** * Lists all alive entities and the number of existing entities for each species * in the bottom left corner.