Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bindings/core/Food_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace py = pybind11;
void init_Food(py::module &m) {
py::class_<Food, EnvironmentObject, std::shared_ptr<Food>>(m, "Food")
.def(py::init<>())
.def(py::init<int>(), py::arg("energy"))
.def("can_be_eaten", &Food::canBeEaten)
.def("eaten", &Food::eaten);
.def("eaten", &Food::eaten)
.def("get_energy", &Food::getEnergy);
}
3 changes: 1 addition & 2 deletions include/core/Environment.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Environment.hpp
#ifndef ENVIRONMENT_HPP
#define ENVIRONMENT_HPP

Expand Down Expand Up @@ -108,7 +107,7 @@ class Environment {
std::unordered_map<boost::uuids::uuid, std::shared_ptr<EnvironmentObject>> objectsMapper;

std::vector<std::shared_ptr<Organism>> deadOrganisms; ///< Accumulated dead organisms
unsigned long foodConsumption; ///< Running food consumption counter
unsigned long foodConsumption = 0; ///< Running food consumption counter

int numThreads = 1; ///< Thread count for the parallelizable reaction phase

Expand Down
47 changes: 39 additions & 8 deletions include/core/EnvironmentObject.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,55 @@
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>

#include "Vec2.hpp"

/**
* @brief Base class for all objects that exist in the simulation environment.
*
* Each object has a unique UUID identifier and a 2D position. Derived classes
* include Organism and Food. Position is stored as Vec2 and is accessible
* both as Vec2 (getPos/setPos) and as std::pair (getPosition/setPosition)
* for backward compatibility.
*/
class EnvironmentObject {
public:
/**
* @brief Construct an environment object at the given coordinates.
* @param x Initial x position.
* @param y Initial y position.
*/
EnvironmentObject(float x, float y)
: id(boost::uuids::random_generator()()), position(std::make_pair(x, y)) {}
: id(boost::uuids::random_generator()()), position(x, y) {}

/** @brief Get the unique identifier of this object. */
boost::uuids::uuid getId() const { return id; }

virtual ~EnvironmentObject() = default;
virtual void postIteration() {};

// [TODO] change this - very bad implementation in order to make organism
// move
virtual std::pair<float, float> getPosition() const { return position; }
virtual void setPosition(float x, float y) { position = std::make_pair(x, y); }
/** @brief Called at the end of each simulation iteration. Override in subclasses. */
virtual void postIteration() {}

/** @brief Get position as a std::pair (legacy interface). */
std::pair<float, float> getPosition() const { return position; }

/**
* @brief Set position from individual coordinates.
* @param x New x coordinate.
* @param y New y coordinate.
*/
void setPosition(float x, float y) { position = Vec2(x, y); }

/** @brief Get position as a Vec2. */
Vec2 getPos() const { return position; }

/** @brief Set position from a Vec2. */
void setPos(Vec2 pos) { position = pos; }

private:
boost::uuids::uuid id;
std::pair<float, float> position;
boost::uuids::uuid id; ///< Unique identifier for spatial-index lookups

protected:
Vec2 position; ///< Current position (accessible to subclasses)
};

#endif
19 changes: 13 additions & 6 deletions include/core/Food.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@ enum class FoodState { FRESH, EATEN };
/**
* @brief A consumable food item that organisms can eat to gain energy.
*
* Food objects exist in the environment and provide a fixed amount of energy
* when consumed. Once eaten, the food transitions to the EATEN state and will
* be cleaned up by the environment. Uses atomic state to allow safe concurrent
* reads during the parallelizable reaction phase.
* Food objects exist in the environment and provide energy when consumed. Once
* eaten, the food transitions to the EATEN state and will be cleaned up by the
* environment. Uses atomic state to allow safe concurrent reads during the
* parallelizable reaction phase.
*/
class Food : public EnvironmentObject {
public:
/** @brief Construct a Food object at the origin with default energy (500). */
Food() : EnvironmentObject(0, 0) {}

/**
* @brief Construct food with a custom energy value.
* @param energy The amount of energy this food provides when consumed.
*/
explicit Food(int energy) : EnvironmentObject(0, 0), energy(energy) {}

/**
* @brief Check whether this food is still available for consumption.
* @return true if the food has not yet been eaten.
Expand All @@ -39,13 +45,14 @@ class Food : public EnvironmentObject {

/**
* @brief Get the energy value this food provides when consumed.
* @return Fixed energy value (500) added to the consuming organism's lifespan.
* @return Energy added to the consuming organism's lifespan.
*/
int getEnergy() const { return 500; }
int getEnergy() const { return energy; }

private:
/// Atomic state ensures safe concurrent access from the parallel reaction phase.
std::atomic<FoodState> state{FoodState::FRESH};
int energy = 500; ///< Energy granted to the organism that eats this food
};

#endif
4 changes: 2 additions & 2 deletions include/core/Organism.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ class Organism : public EnvironmentObject {
*/
double calculateDistance(const std::shared_ptr<EnvironmentObject> &object) const;

std::pair<float, float> movement; ///< Current movement direction vector
int reactionCounter = 0; ///< Guards against multiple reactions per tick
Vec2 movement; ///< Current movement direction vector
int reactionCounter = 0; ///< Guards against multiple reactions per tick

/**
* @brief Apply the current movement vector to update position.
Expand Down
63 changes: 63 additions & 0 deletions include/core/Vec2.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#ifndef VEC2_HPP
#define VEC2_HPP

#include <cmath>
#include <utility>

/**
* @brief A 2D vector struct used for positions and movement directions.
*
* Provides basic vector arithmetic, normalization, and implicit conversion
* to/from std::pair<float, float> for backward compatibility with legacy APIs.
*/
struct Vec2 {
float x = 0.0f; ///< X component
float y = 0.0f; ///< Y component

/// @brief Default constructor, initializes to (0, 0).
Vec2() = default;

/**
* @brief Construct a Vec2 with given x and y components.
* @param x The x component.
* @param y The y component.
*/
Vec2(float x, float y) : x(x), y(y) {}

/// @brief Implicit conversion from std::pair for backward compatibility.
Vec2(const std::pair<float, float>& p) : x(p.first), y(p.second) {}

/// @brief Implicit conversion to std::pair for backward compatibility.
operator std::pair<float, float>() const { return {x, y}; }

/// @brief Component-wise addition.
Vec2 operator+(const Vec2& other) const { return {x + other.x, y + other.y}; }

/// @brief Component-wise subtraction.
Vec2 operator-(const Vec2& other) const { return {x - other.x, y - other.y}; }

/// @brief Scalar multiplication.
Vec2 operator*(float scalar) const { return {x * scalar, y * scalar}; }

/// @brief Compute the Euclidean length of the vector.
float length() const { return std::sqrt(x * x + y * y); }

/**
* @brief Return a vector clamped to a maximum length.
* @param maxLength The maximum allowed length.
* @return A scaled-down copy if length exceeds maxLength, otherwise *this.
*/
Vec2 normalized(float maxLength) const {
float len = length();
if (len > maxLength && len > 0) {
float scale = maxLength / len;
return {x * scale, y * scale};
}
return *this;
}

/// @brief Check whether both components are exactly zero.
bool isZero() const { return x == 0.0f && y == 0.0f; }
};

#endif
22 changes: 17 additions & 5 deletions src/core/Environment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Environment::Environment(int width, int height, std::string type, int numThreads
if (type == "default") {
spatialIndex = std::make_unique<DefaultSpatialIndex<boost::uuids::uuid>>();
} else if (type == "optimized") {
// Use the longest side as the quadtree grid dimension
unsigned long size = static_cast<unsigned long>(std::max(width, height));
spatialIndex = std::make_unique<OptimizedSpatialIndex<boost::uuids::uuid>>(size);
} else {
Expand Down Expand Up @@ -163,7 +164,8 @@ void Environment::simulateIteration(int iterations,
* @brief Remove dead organisms and consumed food from the environment.
*
* Dead organisms are archived in deadOrganisms for post-simulation analysis.
* Consumed food increments the foodConsumption counter.
* Consumed food increments the foodConsumption counter. Uses deferred removal
* to avoid invalidating the iterator during traversal.
*/
void Environment::cleanUp() {
std::vector<boost::uuids::uuid> toRemove;
Expand Down Expand Up @@ -224,19 +226,25 @@ void Environment::updatePositionsInSpatialIndex() {
}
}

// Interactions mutate shared state (food eaten, organism killed, lifeSpan changes),
// so this phase runs single-threaded to avoid data races.
/**
* @brief Run the interaction phase: organisms eat food and fight.
*
* Interactions mutate shared state (food eaten, organism killed, lifeSpan changes),
* so this phase runs single-threaded to avoid data races.
*/
void Environment::handleInteractions() {
auto organisms = getAllOrganisms();

for (auto& organism : organisms) {
if (organism->isAlive()) {
auto position = organism->getPosition();
// Query spatial index for objects within this organism's body size radius
auto interactables =
spatialIndex->query(position.first, position.second, organism->getSize());
std::vector<std::shared_ptr<EnvironmentObject>> interactableObjects;

for (auto& interactable : interactables) {
// Exclude self from interaction targets
if (interactable != organism->getId()) {
auto it = objectsMapper.find(interactable);
if (it != objectsMapper.end()) {
Expand All @@ -250,8 +258,12 @@ void Environment::handleInteractions() {
}
}

// Reactions only write to each organism's own movement/reactionCounter fields,
// so this phase is safe to parallelize across organisms.
/**
* @brief Run the reaction phase: organisms decide movement direction.
*
* Reactions only write to each organism's own movement/reactionCounter fields,
* so this phase is safe to parallelize across organisms.
*/
void Environment::handleReactions() {
auto organisms = getAllOrganisms();
if (organisms.empty()) return;
Expand Down
7 changes: 2 additions & 5 deletions src/core/Genes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
#include <functional>
#include <random>

Genes::Genes(const char *dnaStr) : Genes(dnaStr, nullptr) {
std::memcpy(dna, dnaStr, 4);
}
Genes::Genes(const char *dnaStr) : Genes(dnaStr, nullptr) {}

Genes::Genes(const char *dnaStr, MutationFunction customMutationLogic = nullptr)
: mutationLogic(customMutationLogic ? customMutationLogic : defaultMutationLogic) {
Expand All @@ -26,8 +24,7 @@ void Genes::defaultMutationLogic(char dna[4]) {
std::uniform_int_distribution<int> distribution(-3, 3);

for (int i = 0; i < 4; ++i) {
int mutation = distribution(rng);
dna[i] += mutation;
dna[i] += distribution(rng);
}
}

Expand Down
24 changes: 7 additions & 17 deletions src/core/Organism.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#include <core/Food.hpp>
#include <core/Organism.hpp>
#include <cstdio>
#include <random>

Organism::Organism() : EnvironmentObject(0, 0), genes("\x14\x14\x14\x14"), lifeSpan(500) {}
Expand Down Expand Up @@ -111,13 +110,11 @@ void Organism::react(const std::vector<std::shared_ptr<EnvironmentObject>>& reac
auto otherPos = otherOrganism->getPosition();
if (getSize() * 1.5 < otherOrganism->getSize()) {
// Flee: move away from a much larger predator
movement = std::make_pair(myPos.first - otherPos.first,
myPos.second - otherPos.second);
movement = Vec2(myPos.first - otherPos.first, myPos.second - otherPos.second);
reactionCounter++;
} else if (getSize() > 1.5 * otherOrganism->getSize()) {
// Chase: move toward a much smaller prey
movement = std::make_pair(otherPos.first - myPos.first,
otherPos.second - myPos.second);
movement = Vec2(otherPos.first - myPos.first, otherPos.second - myPos.second);
reactionCounter++;
}
} else if (auto food = std::dynamic_pointer_cast<Food>(nearestObject)) {
Expand All @@ -128,8 +125,7 @@ void Organism::react(const std::vector<std::shared_ptr<EnvironmentObject>>& reac
reactionCounter++;
// Move toward the food source
auto foodPos = food->getPosition();
movement = std::make_pair(foodPos.first - myPos.first,
foodPos.second - myPos.second);
movement = Vec2(foodPos.first - myPos.first, foodPos.second - myPos.second);
} else {
throw std::runtime_error("Unknown object type detected");
}
Expand Down Expand Up @@ -210,25 +206,19 @@ void Organism::makeMove() {
if (reactionCounter == 0) {
// 80% chance to keep current movement direction (4 out of 5 outcomes)
bool keepMovement = dis(gen) > 0;
if (movement.first == 0 && movement.second == 0) {
if (movement.isZero()) {
keepMovement = false;
}

if (!keepMovement) {
movement = std::make_pair(move(gen) * speed, move(gen) * speed);
movement = Vec2(move(gen) * speed, move(gen) * speed);
}
}

// Normalize movement vector to organism's speed
auto movementLength =
std::sqrt(movement.first * movement.first + movement.second * movement.second);
if (movementLength > speed) {
double scalingFactor = speed / movementLength;
movement.first *= scalingFactor;
movement.second *= scalingFactor;
}
movement = movement.normalized(speed);

setPosition(getPosition().first + movement.first, getPosition().second + movement.second);
setPosition(getPosition().first + movement.x, getPosition().second + movement.y);

reactionCounter = 0;
}