diff --git a/bindings/core/Food_bindings.cpp b/bindings/core/Food_bindings.cpp index 2bc9cab..4b8b613 100644 --- a/bindings/core/Food_bindings.cpp +++ b/bindings/core/Food_bindings.cpp @@ -7,6 +7,8 @@ namespace py = pybind11; void init_Food(py::module &m) { py::class_>(m, "Food") .def(py::init<>()) + .def(py::init(), py::arg("energy")) .def("can_be_eaten", &Food::canBeEaten) - .def("eaten", &Food::eaten); + .def("eaten", &Food::eaten) + .def("get_energy", &Food::getEnergy); } diff --git a/include/core/Environment.hpp b/include/core/Environment.hpp index 2b1b4ac..e34b589 100644 --- a/include/core/Environment.hpp +++ b/include/core/Environment.hpp @@ -1,4 +1,3 @@ -// Environment.hpp #ifndef ENVIRONMENT_HPP #define ENVIRONMENT_HPP @@ -108,7 +107,7 @@ class Environment { std::unordered_map> objectsMapper; std::vector> 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 diff --git a/include/core/EnvironmentObject.hpp b/include/core/EnvironmentObject.hpp index 12d7e28..f941e4b 100644 --- a/include/core/EnvironmentObject.hpp +++ b/include/core/EnvironmentObject.hpp @@ -4,24 +4,55 @@ #include #include +#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 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 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 position; + boost::uuids::uuid id; ///< Unique identifier for spatial-index lookups + +protected: + Vec2 position; ///< Current position (accessible to subclasses) }; #endif diff --git a/include/core/Food.hpp b/include/core/Food.hpp index fb335d3..8c345b6 100644 --- a/include/core/Food.hpp +++ b/include/core/Food.hpp @@ -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. @@ -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 state{FoodState::FRESH}; + int energy = 500; ///< Energy granted to the organism that eats this food }; #endif diff --git a/include/core/Organism.hpp b/include/core/Organism.hpp index ee77d8b..e40b73c 100644 --- a/include/core/Organism.hpp +++ b/include/core/Organism.hpp @@ -133,8 +133,8 @@ class Organism : public EnvironmentObject { */ double calculateDistance(const std::shared_ptr &object) const; - std::pair 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. diff --git a/include/core/Vec2.hpp b/include/core/Vec2.hpp new file mode 100644 index 0000000..bc3d37c --- /dev/null +++ b/include/core/Vec2.hpp @@ -0,0 +1,63 @@ +#ifndef VEC2_HPP +#define VEC2_HPP + +#include +#include + +/** + * @brief A 2D vector struct used for positions and movement directions. + * + * Provides basic vector arithmetic, normalization, and implicit conversion + * to/from std::pair 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& p) : x(p.first), y(p.second) {} + + /// @brief Implicit conversion to std::pair for backward compatibility. + operator std::pair() 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 diff --git a/src/core/Environment.cpp b/src/core/Environment.cpp index a58f32f..088a83a 100644 --- a/src/core/Environment.cpp +++ b/src/core/Environment.cpp @@ -21,6 +21,7 @@ Environment::Environment(int width, int height, std::string type, int numThreads if (type == "default") { spatialIndex = std::make_unique>(); } else if (type == "optimized") { + // Use the longest side as the quadtree grid dimension unsigned long size = static_cast(std::max(width, height)); spatialIndex = std::make_unique>(size); } else { @@ -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 toRemove; @@ -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> interactableObjects; for (auto& interactable : interactables) { + // Exclude self from interaction targets if (interactable != organism->getId()) { auto it = objectsMapper.find(interactable); if (it != objectsMapper.end()) { @@ -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; diff --git a/src/core/Genes.cpp b/src/core/Genes.cpp index 0015ab8..2856abf 100644 --- a/src/core/Genes.cpp +++ b/src/core/Genes.cpp @@ -3,9 +3,7 @@ #include #include -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) { @@ -26,8 +24,7 @@ void Genes::defaultMutationLogic(char dna[4]) { std::uniform_int_distribution distribution(-3, 3); for (int i = 0; i < 4; ++i) { - int mutation = distribution(rng); - dna[i] += mutation; + dna[i] += distribution(rng); } } diff --git a/src/core/Organism.cpp b/src/core/Organism.cpp index e88fa83..ac6bc09 100644 --- a/src/core/Organism.cpp +++ b/src/core/Organism.cpp @@ -1,6 +1,5 @@ #include #include -#include #include Organism::Organism() : EnvironmentObject(0, 0), genes("\x14\x14\x14\x14"), lifeSpan(500) {} @@ -111,13 +110,11 @@ void Organism::react(const std::vector>& 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(nearestObject)) { @@ -128,8 +125,7 @@ void Organism::react(const std::vector>& 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"); } @@ -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; }