Skip to content
Open
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
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Ant Simulation - JavaScript Port

This repository originally contains a Unity project implementing an ant simulation. The goal is to build a JavaScript version that runs directly in the browser.

## Project Plan

1. Create a `js` directory to hold the JavaScript implementation.
2. Use vanilla JavaScript and the HTML5 Canvas API for rendering.
3. Mirror the behaviour of the Unity project: ants search for food, leave pheromone trails, and return to the nest.
4. Keep dependencies minimal. Use `npm` to manage any packages.
5. Provide a setup script (`setup.sh`) that prepares the directory structure and checks that Node.js is available.

## Guidelines

- Keep JavaScript code in the `js` directory.
- Use ES6 modules and features when possible.
- Document major features and decisions in `js/README.md`.


## Task and Progress Tracking

- Keep a list of upcoming tasks in a "Tasks" section below.
- Maintain a "Progress" section below recording completed work.
- Update both sections as you work on the project.

### Tasks

### Progress
389 changes: 389 additions & 0 deletions js/Ant.js

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions js/AntColony.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Depends on Vector2.js, AntSettings.js, PerceptionMap.js, Ant.js

class AntColony {
constructor({
settings, // AntSettings instance
position = new Vector2(0, 0), // Colony center position
radius = 2, // Radius of the colony nest area
numToSpawn = 50,
replenishDead = true,
worldSize = { x: 80, y: 60 } // Default world size for perception maps
}) {
this.settings = settings;
this.position = position;
this.radius = radius; // For spawning ants within this area, and as home base
this.numToSpawn = numToSpawn;
this.replenishDead = replenishDead;
this.worldSize = worldSize; // Used for PerceptionMap area

this.ants = [];
this.numFoodCollected = 0;
this.timePassed = 0; // Optional, as in C#

// Initialize PerceptionMaps for pheromones
// The area for these maps should ideally cover the entire simulation space.
this.homeMarkers = new PerceptionMap(this.worldSize, this.settings);
this.foodMarkers = new PerceptionMap(this.worldSize, this.settings);

// Spawn initial ants
this.init();
}

init() {
for (let i = 0; i < this.numToSpawn; i++) {
this.spawnAnt();
}
}

update(deltaTime, foodSources) { // foodSources will be an array of FoodSource instances
this.timePassed += deltaTime;

// Update all ants
// Iterate backwards to allow safe removal of ants during the loop
for (let i = this.ants.length - 1; i >= 0; i--) {
const ant = this.ants[i];
ant.update(deltaTime, foodSources);
// Ant death is handled within ant.update by calling colony.removeAnt(this)
}

// Replenish dead ants
if (this.replenishDead && this.ants.length < this.numToSpawn) {
this.spawnAnt();
}
}

spawnAnt() {
// Spawn ant at a random position within the colony's radius
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * this.radius;
const spawnPos = this.position.add(new Vector2(Math.cos(angle) * dist, Math.sin(angle) * dist));

// Random initial forward direction
const randomAngle = Math.random() * Math.PI * 2;
const initialForwardDir = new Vector2(Math.cos(randomAngle), Math.sin(randomAngle));

const newAnt = new Ant(this.settings, spawnPos, this, initialForwardDir);
this.ants.push(newAnt);
}

// Called by an Ant when it successfully "drops" food at the colony
registerCollectedFood(foodItem) {
this.numFoodCollected++;
// UI update would happen elsewhere, e.g., in a main simulation render loop
// console.log(`Food collected! Total: ${this.numFoodCollected}`);
}

// Called by an Ant when it "collects" food from a FoodSource
// This allows the colony to manage access to food if necessary,
// or simply pass the request to the food source.
requestFoodConsumption(foodItem) {
// Find the food source that owns this foodItem
// This is a bit inefficient; ideally, foodItem would know its source or be globally unique.
// For now, assuming foodSources is available and not too large.
// In the C# version, AntManager calls FoodSource.ConsumeFood.
// Here, the Ant asks the Colony, which might ask the FoodSource.

// Simplified: find the food source that contains this food item and tell it to consume it.
// This requires foodSources to be accessible here, or a different pattern.
// Let's assume `foodSources` is passed to `update` and available if needed,
// but for now, the Ant directly marks food as `isCollected`. The actual removal from
// the FoodSource's list might need to happen in the main loop or via the Ant itself
// if it holds a reference to the FoodSource.
// For now, we'll assume the Ant handles its `targetFood.isCollected = true`
// and the FoodSource might later clean up collected items or the Ant tells it.

// Let's refine this: The Ant has a targetFood. It should ask that FoodSource to consume it.
// The Ant should call targetFood.source.consumeFood(targetFood).
// This method in Colony might not be strictly necessary if Ants manage consumption directly.
// However, if the Colony needs to mediate or track this, it can.

// For this port, let's assume the Ant has already marked the food as "collected" (isCollected = true).
// The colony's role here is mostly to acknowledge.
// The actual removal from the FoodSource's list can be handled by the FoodSource itself
// when an ant tries to pick up an already collected item, or periodically.

// This method is less critical if ants directly interact with FoodSource for consumption.
// Let's keep it simple for now. The critical part is `registerCollectedFood`.
// C# AntManager calls foodSource.ConsumeFood(ant.TargetFood);
// So, the ant should probably tell the food source directly.
// Let's assume `ant.collectFood()` handles this interaction.
// This method can return true to confirm consumption is allowed/successful.
return true; // Placeholder, actual consumption logic is more complex.
}

// Called by an Ant when it dies
removeAnt(antToRemove) {
const index = this.ants.indexOf(antToRemove);
if (index > -1) {
this.ants.splice(index, 1);
// console.log("Ant died and was removed from colony.");
}
}
}

// Export for use in other modules if using a module system (e.g., ES6 modules)
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = AntColony;
}
37 changes: 37 additions & 0 deletions js/AntSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const AntSettings = {
// Movement
maxSpeed: 2,
acceleration: 3,
collisionAvoidSteerStrength: 5,
targetSteerStrength: 3,
randomSteerStrength: 0.6,
randomSteerMaxDuration: 1,
timeBetweenDirUpdate: 0.15,
collisionRadius: 0.15,

// Pheromones
dstBetweenMarkers: 0.75,
pheromoneEvaporateTime: 45,
pheromoneRunOutTime: 30,
pheromoneWeight: 1,
perceptionRadius: 2.5,
useHomeMarkers: true,
useFoodMarkers: true,

// Sensing
sensorSize: 0.75,
sensorDst: 1.25,
sensorSpacing: 1,
antennaDst: 0.25,

// Lifetime
lifetime: 150,
useDeath: false,
};

// Export for use in other modules if using a module system (e.g., ES6 modules)
// For simple browser environments, this will make AntSettings globally available.
// If you intend to use ES6 modules, you would use: export default AntSettings;
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = AntSettings;
}
113 changes: 113 additions & 0 deletions js/FoodSource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Depends on Vector2.js

class FoodItem {
constructor(position) {
this.position = position; // Vector2
this.size = 5; // Default size, can be customized
// In a real scenario, this might hold more info (e.g., graphic, type)
}
}

class FoodSource {
constructor({
position = new Vector2(0, 0),
radius = 10,
// foodPrefab is conceptual here; we'll create FoodItem instances
timeBetweenSpawns = 1, // Not directly used if spawning is driven by update/maintainAmount
amount = 50,
maintainAmount = true,
blobCount = 3,
seed = 0 // Note: Math.random() in JS is not directly seedable. This seed is for potential future PRNG.
} = {}) {
this.position = position; // Center position of the food source area
this.radius = radius;
this.amount = amount; // Target number of food items
this.maintainAmount = maintainAmount;
this.blobCount = blobCount;
this.seed = seed; // Store seed

// Simple PRNG (Linear Congruential Generator - LCG)
// Parameters from https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
this.prng_m = 0x80000000; // 2^31
this.prng_a = 1103515245;
this.prng_c = 12345;
this.prng_state = seed;

this.foodItems = [];
this.blobs = [];

this.initBlobs();

// Initial spawn
for (let i = 0; i < this.amount; i++) {
this.spawnFood();
}
}

// Simple LCG random number generator (returns value between 0 and 1)
random() {
this.prng_state = (this.prng_a * this.prng_state + this.prng_c) % this.prng_m;
return this.prng_state / (this.prng_m -1);
}

initBlobs() {
const blobRadiusMin = this.radius * 0.1;
const blobRadiusMax = this.radius * 0.4;

for (let i = 0; i < this.blobCount; i++) {
const angle = this.random() * Math.PI * 2;
const dist = this.random() * this.radius * 0.75; // Place blobs not too close to the edge
const blobPos = this.position.add(new Vector2(Math.cos(angle) * dist, Math.sin(angle) * dist));
const blobRadius = blobRadiusMin + this.random() * (blobRadiusMax - blobRadiusMin);
this.blobs.push({ position: blobPos, radius: blobRadius });
}
// If no blobs, make the source itself a blob
if (this.blobs.length === 0) {
this.blobs.push({ position: this.position.clone(), radius: this.radius });
}
}

spawnFood() {
if (this.blobs.length === 0) {
console.error("No blobs to spawn food in.");
return null;
}

// Pick a random blob
const blobIndex = Math.floor(this.random() * this.blobs.length);
const selectedBlob = this.blobs[blobIndex];

// Generate a random point within the selected blob's radius
const angle = this.random() * Math.PI * 2;
const dist = this.random() * selectedBlob.radius;
const spawnPos = selectedBlob.position.add(new Vector2(Math.cos(angle) * dist, Math.sin(angle) * dist));

const newFood = new FoodItem(spawnPos);
this.foodItems.push(newFood);
return newFood;
}

// update method to maintain food amount
update() {
if (this.maintainAmount && this.foodItems.length < this.amount) {
this.spawnFood();
}
// In a simulation, this might also handle food consumption, etc.
}

// Method to consume food, e.g., by an ant
consumeFood(foodItem) {
const index = this.foodItems.indexOf(foodItem);
if (index > -1) {
this.foodItems.splice(index, 1);
return true; // Food consumed
}
return false; // Food not found
}
}

// Export for use in other modules if using a module system (e.g., ES6 modules)
// For simple browser environments, this will make FoodSource and FoodItem globally available.
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = { FoodSource, FoodItem };
}
Loading