Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Advanced Ant Colony Simulation</title> | |
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;600&display=swap" rel="stylesheet"> | |
<style> | |
:root { | |
--colony-color: #5D4037; | |
--worker-color: #FF5722; | |
--scout-color: #2196F3; | |
--queen-color: #9C27B0; | |
--food-color: #4CAF50; | |
--pheromone-color: rgba(255, 235, 59, 0.5); | |
--pheromone-home-color: rgba(255, 192, 203, 0.5); | |
--bg-color: #F5DEB3; | |
--text-color: #5D4037; | |
--panel-color: #FFF8DC; | |
} | |
body { | |
font-family: 'Quicksand', sans-serif; | |
margin: 0; | |
padding: 20px; | |
background-color: var(--bg-color); | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
color: var(--text-color); | |
overflow-x: hidden; | |
} | |
h1 { | |
color: var(--colony-color); | |
text-shadow: 1px 1px 3px rgba(0,0,0,0.2); | |
margin-bottom: 15px; | |
font-weight: 600; | |
} | |
.header { | |
position: relative; | |
width: 100%; | |
max-width: 850px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 15px; | |
} | |
.controls { | |
display: flex; | |
gap: 12px; | |
flex-wrap: wrap; | |
justify-content: center; | |
} | |
button { | |
padding: 8px 16px; | |
background-color: var(--colony-color); | |
color: white; | |
border: none; | |
border-radius: 20px; | |
cursor: pointer; | |
transition: all 0.3s; | |
font-family: 'Quicksand', sans-serif; | |
font-weight: 600; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
} | |
button:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.3); | |
} | |
button:active { | |
transform: translateY(0); | |
} | |
button.secondary { | |
background-color: #8D6E63; | |
} | |
.speed-control { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
margin-left: 20px; | |
} | |
.speed-control label { | |
font-weight: 600; | |
} | |
.stats-container { | |
width: 100%; | |
max-width: 850px; | |
background-color: var(--panel-color); | |
border-radius: 15px; | |
box-shadow: 0 3px 10px rgba(0,0,0,0.1); | |
padding: 15px; | |
margin-bottom: 20px; | |
} | |
.stats { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 15px 30px; | |
justify-content: center; | |
} | |
.stat-item { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
min-width: 120px; | |
} | |
.stat-label { | |
font-size: 14px; | |
opacity: 0.8; | |
} | |
.stat-value { | |
font-size: 24px; | |
font-weight: 600; | |
color: var(--colony-color); | |
margin-top: 5px; | |
} | |
canvas { | |
background-color: var(--bg-color); | |
border-radius: 10px; | |
box-shadow: 0 5px 15px rgba(0,0,0,0.2); | |
max-width: 100%; | |
margin-bottom: 20px; | |
} | |
.info-panels { | |
display: flex; | |
gap: 20px; | |
width: 100%; | |
max-width: 850px; | |
flex-wrap: wrap; | |
justify-content: center; | |
} | |
.panel { | |
flex: 1; | |
min-width: 250px; | |
background-color: var(--panel-color); | |
border-radius: 15px; | |
box-shadow: 0 3px 10px rgba(0,0,0,0.1); | |
padding: 20px; | |
} | |
.panel h2 { | |
margin-top: 0; | |
color: var(--colony-color); | |
font-size: 18px; | |
border-bottom: 2px solid var(--colony-color); | |
padding-bottom: 10px; | |
margin-bottom: 15px; | |
} | |
.legend { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 15px; | |
margin-top: 10px; | |
} | |
.legend-item { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.legend-color { | |
width: 16px; | |
height: 16px; | |
border-radius: 50%; | |
border: 1px solid rgba(0,0,0,0.2); | |
} | |
.food-source { background-color: var(--food-color); } | |
.ant-worker { background-color: var(--worker-color); } | |
.ant-scout { background-color: var(--scout-color); } | |
.ant-queen { background-color: var(--queen-color); } | |
.pheromone { background-color: var(--pheromone-color); } | |
.pheromone-home { background-color: var(--pheromone-home-color); } | |
.instructions { | |
line-height: 1.6; | |
font-size: 15px; | |
} | |
.instructions ul { | |
padding-left: 20px; | |
} | |
.tabs { | |
display: flex; | |
border-bottom: 1px solid #ddd; | |
margin-bottom: 15px; | |
} | |
.tab { | |
padding: 8px 16px; | |
cursor: pointer; | |
border-bottom: 3px solid transparent; | |
transition: all 0.3s; | |
} | |
.tab.active { | |
border-bottom: 3px solid var(--colony-color); | |
font-weight: 600; | |
} | |
.tab-content { | |
display: none; | |
} | |
.tab-content.active { | |
display: block; | |
} | |
@media (max-width: 600px) { | |
.header { | |
flex-direction: column; | |
gap: 15px; | |
} | |
.speed-control { | |
margin-left: 0; | |
} | |
.stat-item { | |
min-width: 100px; | |
} | |
.stat-value { | |
font-size: 20px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Advanced Ant Colony Simulation</h1> | |
<div class="header"> | |
<div class="controls"> | |
<button id="startBtn">▶ Start</button> | |
<button id="pauseBtn" class="secondary">⏸ Pause</button> | |
<button id="resetBtn" class="secondary">🔄 Reset</button> | |
<button id="addFoodBtn">🍎 Add Food</button> | |
<button id="addAntsBtn">🐜 Add Ants</button> | |
</div> | |
<div class="speed-control"> | |
<label for="speedSlider">Speed:</label> | |
<input type="range" id="speedSlider" min="1" max="10" value="3"> | |
</div> | |
</div> | |
<div class="stats-container"> | |
<div class="stats"> | |
<div class="stat-item"> | |
<span class="stat-label">Total Ants</span> | |
<span class="stat-value" id="antCount">0</span> | |
</div> | |
<div class="stat-item"> | |
<span class="stat-label">Workers</span> | |
<span class="stat-value" id="workerCount">0</span> | |
</div> | |
<div class="stat-item"> | |
<span class="stat-label">Scouts</span> | |
<span class="stat-value" id="scoutCount">0</span> | |
</div> | |
<div class="stat-item"> | |
<span class="stat-label">Food Sources</span> | |
<span class="stat-value" id="foodCount">0</span> | |
</div> | |
<div class="stat-item"> | |
<span class="stat-label">Food Collected</span> | |
<span class="stat-value" id="foodCollected">0</span> | |
</div> | |
<div class="stat-item"> | |
<span class="stat-label">Pheromones</span> | |
<span class="stat-value" id="pheromoneCount">0</span> | |
</div> | |
</div> | |
</div> | |
<canvas id="antCanvas" width="800" height="600"></canvas> | |
<div class="info-panels"> | |
<div class="panel"> | |
<h2>Legend</h2> | |
<div class="legend"> | |
<div class="legend-item"> | |
<div class="legend-color ant-worker"></div> | |
<span>Worker Ants</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color ant-scout"></div> | |
<span>Scout Ants</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color ant-queen"></div> | |
<span>Queen Ant</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color food-source"></div> | |
<span>Food Source</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color pheromone"></div> | |
<span>Food Trail</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color pheromone-home"></div> | |
<span>Home Trail</span> | |
</div> | |
</div> | |
</div> | |
<div class="panel"> | |
<div class="tabs"> | |
<div class="tab active" data-tab="instructions">Instructions</div> | |
<div class="tab" data-tab="about">About</div> | |
</div> | |
<div class="tab-content active" id="instructions"> | |
<div class="instructions"> | |
<ul> | |
<li>Click ▶ <strong>Start</strong> to begin simulation</li> | |
<li>Click 🍎 <strong>Add Food</strong> to place new food sources</li> | |
<li>Click 🐜 <strong>Add Ants</strong> to increase colony size</li> | |
<li>Click anywhere on the map to add food at that location</li> | |
<li>Adjust the speed slider to change simulation speed</li> | |
<li>The queen ant will create new ants when enough food is stored</li> | |
</ul> | |
<p>Watch as ants follow pheromone trails to efficiently gather food!</p> | |
</div> | |
</div> | |
<div class="tab-content" id="about"> | |
<div class="instructions"> | |
<p>This simulation demonstrates swarm intelligence in ant colonies. Key behaviors:</p> | |
<ul> | |
<li><strong>Worker Ants</strong>: Collect food and follow pheromone trails</li> | |
<li><strong>Scout Ants</strong>: Explore randomly to discover new food sources</li> | |
<li><strong>Queen Ant</strong>: Reproduces new ants when the colony has enough food</li> | |
<li><strong>Pheromone Trails</strong>: Used for communication between ants</li> | |
</ul> | |
<p>The simulation uses requestAnimationFrame for smooth performance even with hundreds of ants.</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// DOM Elements | |
const canvas = document.getElementById('antCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const startBtn = document.getElementById('startBtn'); | |
const pauseBtn = document.getElementById('pauseBtn'); | |
const resetBtn = document.getElementById('resetBtn'); | |
const addFoodBtn = document.getElementById('addFoodBtn'); | |
const addAntsBtn = document.getElementById('addAntsBtn'); | |
const speedSlider = document.getElementById('speedSlider'); | |
// Stats elements | |
const antCountDisplay = document.getElementById('antCount'); | |
const workerCountDisplay = document.getElementById('workerCount'); | |
const scoutCountDisplay = document.getElementById('scoutCount'); | |
const foodCountDisplay = document.getElementById('foodCount'); | |
const foodCollectedDisplay = document.getElementById('foodCollected'); | |
const pheromoneCountDisplay = document.getElementById('pheromoneCount'); | |
// Tab functionality | |
document.querySelectorAll('.tab').forEach(tab => { | |
tab.addEventListener('click', () => { | |
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
tab.classList.add('active'); | |
const tabId = tab.getAttribute('data-tab'); | |
document.getElementById(tabId).classList.add('active'); | |
}); | |
}); | |
// Simulation state | |
let isRunning = false; | |
let animationId = null; | |
let lastTime = 0; | |
let simSpeed = 3; | |
let foodCollected = 0; | |
let frameCount = 0; | |
let fps = 0; | |
let lastFpsUpdate = 0; | |
// Configuration | |
const config = { | |
antSize: 3, | |
foodSize: 8, | |
pheromoneSize: 2, | |
colonyRadius: 30, | |
pheromoneDecay: 0.002, | |
maxPheromone: 50, | |
initialAnts: 20, | |
initialScouts: 5, | |
initialFood: 3, | |
foodCapacity: 50, | |
pheromoneDropRate: 0.5, | |
queenSize: 8, | |
antLifespan: 2000, | |
scoutLifespan: 2500, | |
antCost: 3, | |
antSpawnRate: 0.01, | |
scoutRatio: 0.2, | |
wanderStrength: 0.1, | |
followStrength: 0.8, | |
maxAnts: 500, | |
viewDistance: 80, | |
foodDetectionRange: 20, | |
avoidanceRadius: 15 | |
}; | |
// Game entities | |
const colony = { | |
x: canvas.width / 2, | |
y: canvas.height / 2, | |
foodStored: 10, | |
size: 30, | |
tick: 0 | |
}; | |
let ants = []; | |
let scouts = []; | |
let foodSources = []; | |
let pheromones = []; | |
let homePheromones = []; | |
// Event listeners | |
startBtn.addEventListener('click', startSimulation); | |
pauseBtn.addEventListener('click', pauseSimulation); | |
resetBtn.addEventListener('click', resetSimulation); | |
addFoodBtn.addEventListener('click', addRandomFoodSource); | |
addAntsBtn.addEventListener('click', () => { | |
addAnts(10); | |
updateStats(); | |
}); | |
speedSlider.addEventListener('input', (e) => { | |
simSpeed = parseInt(e.target.value); | |
}); | |
// Initialize simulation | |
resetSimulation(); | |
function startSimulation() { | |
if (!isRunning) { | |
isRunning = true; | |
lastTime = performance.now(); | |
animate(lastTime); | |
} | |
} | |
function pauseSimulation() { | |
isRunning = false; | |
if (animationId) { | |
cancelAnimationFrame(animationId); | |
animationId = null; | |
} | |
} | |
function resetSimulation() { | |
pauseSimulation(); | |
// Reset colony | |
colony.foodStored = 10; | |
colony.tick = 0; | |
foodCollected = 0; | |
// Clear all entities | |
ants = []; | |
scouts = []; | |
foodSources = []; | |
pheromones = []; | |
homePheromones = []; | |
// Create initial ants | |
addAnts(config.initialAnts); | |
addScouts(config.initialScouts); | |
// Add queen | |
addQueen(); | |
// Create initial food sources | |
for (let i = 0; i < config.initialFood; i++) { | |
addRandomFoodSource(); | |
} | |
updateStats(); | |
draw(); | |
} | |
function addAnts(count) { | |
for (let i = 0; i < count && ants.length + scouts.length < config.maxAnts; i++) { | |
createAnt('worker'); | |
} | |
} | |
function addScouts(count) { | |
for (let i = 0; i < count && ants.length + scouts.length < config.maxAnts; i++) { | |
createAnt('scout'); | |
} | |
} | |
function createAnt(type) { | |
const ant = { | |
x: colony.x + (Math.random() * 2 - 1) * 10, | |
y: colony.y + (Math.random() * 2 - 1) * 10, | |
size: type === 'queen' ? config.queenSize : config.antSize, | |
type: type, | |
carryingFood: false, | |
target: null, | |
direction: Math.random() * Math.PI * 2, | |
speed: type === 'scout' ? 2.5 : 1.8 + Math.random() * 0.4, | |
life: type === 'scout' ? config.scoutLifespan : config.antLifespan, | |
age: 0, | |
memory: [], | |
lastPheromoneDrop: 0 | |
}; | |
if (type === 'worker') { | |
ants.push(ant); | |
} else if (type === 'scout') { | |
scouts.push(ant); | |
} else if (type === 'queen') { | |
ants.unshift(ant); // Queen is first in array | |
} | |
} | |
function addQueen() { | |
const queen = { | |
x: colony.x, | |
y: colony.y, | |
size: config.queenSize, | |
type: 'queen', | |
speed: 0, | |
life: Infinity, | |
age: 0, | |
lastSpawn: 0 | |
}; | |
ants.unshift(queen); | |
} | |
function addRandomFoodSource() { | |
// Place food randomly but not too close to colony | |
let x, y, distance, tries = 0; | |
do { | |
x = Math.random() * (canvas.width - 100) + 50; | |
y = Math.random() * (canvas.height - 100) + 50; | |
distance = Math.hypot(x - colony.x, y - colony.y); | |
tries++; | |
} while (distance < 150 && tries < 20); | |
foodSources.push({ | |
x: x, | |
y: y, | |
amount: config.foodCapacity * (0.5 + Math.random() * 0.5) | |
}); | |
} | |
function addFoodAtPosition(x, y) { | |
foodSources.push({ | |
x: x, | |
y: y, | |
amount: config.foodCapacity * (0.5 + Math.random() * 0.5) | |
}); | |
} | |
function addPheromone(x, y, strength, type) { | |
if (type === 'food') { | |
pheromones.push({ | |
x: x, | |
y: y, | |
strength: Math.min(strength, config.maxPheromone), | |
type: 'food', | |
life: 1, | |
age: 0 | |
}); | |
} else { | |
homePheromones.push({ | |
x: x, | |
y: y, | |
strength: Math.min(strength, config.maxPheromone), | |
type: 'home', | |
life: 1, | |
age: 0 | |
}); | |
} | |
} | |
function updateStats() { | |
antCountDisplay.textContent = ants.length + scouts.length - 1; // Exclude queen | |
workerCountDisplay.textContent = ants.length - 1; // Exclude queen | |
scoutCountDisplay.textContent = scouts.length; | |
foodCountDisplay.textContent = foodSources.length; | |
foodCollectedDisplay.textContent = foodCollected; | |
pheromoneCountDisplay.textContent = pheromones.length + homePheromones.length; | |
} | |
function update(deltaTime) { | |
// Update time-based calculations with deltaTime for smooth animation | |
const timeFactor = deltaTime / 16; // Normalize to ~60fps | |
colony.tick++; | |
// Update queen behavior | |
if (ants.length > 0 && ants[0].type === 'queen') { | |
const queen = ants[0]; | |
queen.age += timeFactor; | |
// Queen produces new ants when there's enough food | |
if (colony.foodStored >= config.antCost && | |
queen.age - queen.lastSpawn > 100 && | |
Math.random() < config.antSpawnRate * timeFactor) { | |
colony.foodStored -= config.antCost; | |
queen.lastSpawn = queen.age; | |
// Add new ant (worker or scout) | |
if (scouts.length / (ants.length + scouts.length) < config.scoutRatio) { | |
createAnt('scout'); | |
} else { | |
createAnt('worker'); | |
} | |
// Occasionally produce an extra ant | |
if (Math.random() < 0.1) { | |
createAnt(Math.random() < 0.5 ? 'worker' : 'scout'); | |
} | |
} | |
} | |
// Update pheromones (decay and remove weak ones) | |
pheromones = pheromones.map(p => { | |
p.life -= config.pheromoneDecay * timeFactor; | |
p.age += timeFactor; | |
return p; | |
}).filter(p => p.life > 0); | |
homePheromones = homePheromones.map(p => { | |
p.life -= config.pheromoneDecay * timeFactor; | |
p.age += timeFactor; | |
return p; | |
}).filter(p => p.life > 0); | |
// Update workers | |
ants.forEach((ant, index) => { | |
if (ant.type === 'queen') return; | |
ant.age += timeFactor; | |
// Remove dead ants | |
if (ant.age > ant.life) { | |
ants.splice(index, 1); | |
return; | |
} | |
// Mark food sources that have been depleted | |
if (ant.target && ant.target.amount <= 0) { | |
ant.target = null; | |
ant.carryingFood = false; | |
} | |
if (ant.carryingFood) { | |
// Returning to colony with food | |
returnToColony(ant, timeFactor); | |
} else { | |
// Searching for food | |
searchForFood(ant, timeFactor); | |
} | |
}); | |
// Update scouts | |
scouts.forEach((scout, index) => { | |
scout.age += timeFactor; | |
// Remove dead scouts | |
if (scout.age > scout.life) { | |
scouts.splice(index, 1); | |
return; | |
} | |
// Scouts have a small chance to become workers when they report food | |
if (scout.target && Math.random() < 0.01 * timeFactor) { | |
ants.push({ | |
...scout, | |
type: 'worker', | |
speed: 1.8 + Math.random() * 0.4, | |
life: config.antLifespan | |
}); | |
scouts.splice(index, 1); | |
return; | |
} | |
if (scout.carryingFood) { | |
// Scouts returning with food use the same logic as workers | |
returnToColony(scout, timeFactor); | |
} else { | |
// Scouts explore differently - move randomly but can detect food | |
explore(scout, timeFactor); | |
} | |
}); | |
updateStats(); | |
} | |
function returnToColony(ant, timeFactor) { | |
const dx = colony.x - ant.x; | |
const dy = colony.y - ant.y; | |
const distance = Math.hypot(dx, dy); | |
if (distance < config.colonyRadius + ant.size) { | |
// Reached colony, drop food | |
colony.foodStored++; | |
foodCollected++; | |
ant.carryingFood = false; | |
ant.target = null; | |
// Remember this food source location | |
if (ant.memory.length > 5) ant.memory.shift(); | |
if (ant.target) ant.memory.push({x: ant.target.x, y: ant.target.y}); | |
} else { | |
// Drop home pheromone trail periodically | |
if (ant.age - ant.lastPheromoneDrop > 20) { | |
addPheromone(ant.x, ant.y, 1, 'home'); | |
ant.lastPheromoneDrop = ant.age; | |
} | |
// Move toward colony with some path following | |
const desiredDirection = Math.atan2(dy, dx); | |
// Follow home pheromones if available | |
const homeTrail = findBestPheromone(ant.x, ant.y, 'home', ant.direction); | |
if (homeTrail) { | |
const pdx = homeTrail.x - ant.x; | |
const pdy = homeTrail.y - ant.y; | |
const pheromoneDirection = Math.atan2(pdy, pdx); | |
// Blend directions based on pheromone strength | |
ant.direction = blendDirections( | |
desiredDirection, | |
pheromoneDirection, | |
config.followStrength * homeTrail.life | |
); | |
} else { | |
ant.direction = desiredDirection; | |
} | |
// Avoid other ants | |
avoidAnts(ant); | |
// Move ant | |
ant.x += Math.cos(ant.direction) * ant.speed * timeFactor; | |
ant.y += Math.sin(ant.direction) * ant.speed * timeFactor; | |
// Bounce off edges | |
bounceOffWalls(ant); | |
} | |
} | |
function searchForFood(ant, timeFactor) { | |
// If we have a target, go to it | |
if (ant.target) { | |
const dx = ant.target.x - ant.x; | |
const dy = ant.target.y - ant.y; | |
const distance = Math.hypot(dx, dy); | |
if (distance < config.foodDetectionRange) { | |
// Reached food, pick it up if there's any left | |
if (ant.target.amount > 0) { | |
ant.carryingFood = true; | |
ant.target.amount--; | |
// Remove empty food sources | |
if (ant.target.amount <= 0) { | |
foodSources = foodSources.filter(f => f !== ant.target); | |
} | |
} else { | |
ant.target = null; | |
} | |
} else { | |
// Drop food pheromone trail periodically if we have a target | |
if (ant.age - ant.lastPheromoneDrop > 20) { | |
addPheromone(ant.x, ant.y, 1, 'food'); | |
ant.lastPheromoneDrop = ant.age; | |
} | |
// Move toward food | |
const desiredDirection = Math.atan2(dy, dx); | |
// Follow food pheromones if available | |
const foodTrail = findBestPheromone(ant.x, ant.y, 'food', desiredDirection); | |
if (foodTrail) { | |
const pdx = foodTrail.x - ant.x; | |
const pdy = foodTrail.y - ant.y; | |
const pheromoneDirection = Math.atan2(pdy, pdx); | |
ant.direction = blendDirections( | |
desiredDirection, | |
pheromoneDirection, | |
config.followStrength * foodTrail.life | |
); | |
} else { | |
ant.direction = desiredDirection; | |
} | |
// Avoid other ants | |
avoidAnts(ant); | |
// Add some random wandering | |
ant.direction += (Math.random() - 0.5) * config.wanderStrength * timeFactor; | |
// Move ant | |
ant.x += Math.cos(ant.direction) * ant.speed * timeFactor; | |
ant.y += Math.sin(ant.direction) * ant.speed * timeFactor; | |
// Bounce off edges | |
bounceOffWalls(ant); | |
} | |
} else { | |
// No target - find one | |
ant.target = findClosestFood(ant.x, ant.y); | |
// Detect nearby food directly if we don't have a target | |
if (!ant.target) { | |
ant.target = detectNearbyFood(ant.x, ant.y); | |
} | |
// If we still don't have a target, follow pheromone trails or wander | |
if (!ant.target) { | |
const foodTrail = findBestPheromone(ant.x, ant.y, 'food'); | |
if (foodTrail) { | |
// Move toward strongest pheromone trail | |
const dx = foodTrail.x - ant.x; | |
const dy = foodTrail.y - ant.y; | |
ant.direction = Math.atan2(dy, dx); | |
} else if (ant.memory.length > 0) { | |
// Check remembered locations | |
const memory = ant.memory[Math.floor(Math.random() * ant.memory.length)]; | |
const dx = memory.x - ant.x; | |
const dy = memory.y - ant.y; | |
ant.direction = Math.atan2(dy, dx); | |
// Remove old memories occasionally | |
if (Math.random() < 0.001 * timeFactor) { | |
ant.memory.shift(); | |
} | |
} else { | |
// Random wandering | |
ant.direction += (Math.random() - 0.5) * config.wanderStrength * timeFactor; | |
} | |
// Avoid other ants | |
avoidAnts(ant); | |
// Move ant | |
ant.x += Math.cos(ant.direction) * ant.speed * timeFactor; | |
ant.y += Math.sin(ant.direction) * ant.speed * timeFactor; | |
// Bounce off edges | |
bounceOffWalls(ant); | |
} | |
} | |
} | |
function explore(scout, timeFactor) { | |
// If we found food, target it | |
if (!scout.target) { | |
scout.target = detectNearbyFood(scout.x, scout.y); | |
} | |
if (scout.target) { | |
// We found food! Act like a worker now | |
const dx = scout.target.x - scout.x; | |
const dy = scout.target.y - scout.y; | |
const distance = Math.hypot(dx, dy); | |
if (distance < config.foodDetectionRange) { | |
// Pick up food if available | |
if (scout.target.amount > 0) { | |
scout.carryingFood = true; | |
scout.target.amount--; | |
// Remove empty food sources | |
if (scout.target.amount <= 0) { | |
foodSources = foodSources.filter(f => f !== scout.target); | |
scout.target = null; | |
} | |
} else { | |
scout.target = null; | |
} | |
} else { | |
// Move toward food | |
scout.direction = Math.atan2(dy, dx); | |
} | |
} else { | |
// No food found, keep exploring | |
if (Math.random() < 0.02 * timeFactor) { | |
scout.direction += (Math.random() - 0.5) * Math.PI; | |
} | |
// Occasionally check for pheromones | |
if (Math.random() < 0.01 * timeFactor) { | |
const foodTrail = findBestPheromone(scout.x, scout.y, 'food'); | |
if (foodTrail) { | |
const dx = foodTrail.x - scout.x; | |
const dy = foodTrail.y - scout.y; | |
scout.direction = Math.atan2(dy, dx); | |
} | |
} | |
} | |
// Avoid other ants | |
avoidAnts(scout); | |
// Move scout | |
scout.x += Math.cos(scout.direction) * scout.speed * timeFactor; | |
scout.y += Math.sin(scout.direction) * scout.speed * timeFactor; | |
// Bounce off edges | |
bounceOffWalls(scout); | |
// Occasionally drop a pheromone if carrying food | |
if (scout.carryingFood && Math.random() < 0.1 * timeFactor) { | |
addPheromone(scout.x, scout.y, 1, 'home'); | |
} | |
} | |
function findClosestFood(x, y) { | |
let closestFood = null; | |
let minDistance = Infinity; | |
for (const food of foodSources) { | |
if (food.amount > 0) { | |
const dx = food.x - x; | |
const dy = food.y - y; | |
const distance = Math.hypot(dx, dy); | |
if (distance < minDistance) { | |
minDistance = distance; | |
closestFood = food; | |
} | |
} | |
} | |
return minDistance < 300 ? closestFood : null; // Don't go too far | |
} | |
function detectNearbyFood(x, y) { | |
for (const food of foodSources) { | |
if (food.amount > 0) { | |
const dx = food.x - x; | |
const dy = food.y - y; | |
const distance = Math.hypot(dx, dy); | |
if (distance < config.viewDistance * 1.5) { | |
return food; | |
} | |
} | |
} | |
return null; | |
} | |
function findBestPheromone(x, y, type, currentDirection) { | |
const relevantPheromones = type === 'food' ? pheromones : homePheromones; | |
let bestPheromone = null; | |
let bestScore = -Infinity; | |
for (const p of relevantPheromones) { | |
const dx = p.x - x; | |
const dy = p.y - y; | |
const distance = Math.hypot(dx, dy); | |
if (distance < config.viewDistance && distance > 5) { | |
// Score based on strength, distance, and alignment with current direction | |
let score = p.life * (1 - distance / config.viewDistance); | |
if (currentDirection !== undefined) { | |
const pheromoneDirection = Math.atan2(dy, dx); | |
const directionDiff = Math.abs(normalizeAngle(pheromoneDirection - currentDirection)); | |
const directionScore = 1 - directionDiff / Math.PI; | |
score *= 1 + directionScore * 0.5; | |
} | |
if (score > bestScore) { | |
bestScore = score; | |
bestPheromone = p; | |
} | |
} | |
} | |
return bestPheromone; | |
} | |
function avoidAnts(ant) { | |
// Combine all ants for checking | |
const allAnts = [...ants, ...scouts].filter(a => a !== ant); | |
// Avoid other ants | |
let avoidanceX = 0; | |
let avoidanceY = 0; | |
let avoidanceCount = 0; | |
for (const otherAnt of allAnts) { | |
const dx = otherAnt.x - ant.x; | |
const dy = otherAnt.y - ant.y; | |
const distance = Math.hypot(dx, dy); | |
if (distance < config.avoidanceRadius) { | |
// Push away from other ant | |
avoidanceX -= dx / distance; | |
avoidanceY -= dy / distance; | |
avoidanceCount++; | |
} | |
} | |
if (avoidanceCount > 0) { | |
const avoidanceDirection = Math.atan2(avoidanceY, avoidanceX); | |
ant.direction = blendDirections(ant.direction, avoidanceDirection, 0.5); | |
} | |
} | |
function bounceOffWalls(ant) { | |
// Bounce off edges with some randomness | |
if (ant.x < 0) { | |
ant.x = 0; | |
ant.direction = Math.PI - ant.direction + (Math.random() - 0.5) * 0.5; | |
} else if (ant.x > canvas.width) { | |
ant.x = canvas.width; | |
ant.direction = Math.PI - ant.direction + (Math.random() - 0.5) * 0.5; | |
} | |
if (ant.y < 0) { | |
ant.y = 0; | |
ant.direction = -ant.direction + (Math.random() - 0.5) * 0.5; | |
} else if (ant.y > canvas.height) { | |
ant.y = canvas.height; | |
ant.direction = -ant.direction + (Math.random() - 0.5) * 0.5; | |
} | |
} | |
function blendDirections(dir1, dir2, strength) { | |
// Normalize both angles | |
const a1 = normalizeAngle(dir1); | |
const a2 = normalizeAngle(dir2); | |
// Calculate blended angle | |
let blended; | |
if (Math.abs(a1 - a2) <= Math.PI) { | |
blended = a1 * (1 - strength) + a2 * strength; | |
} else { | |
if (a1 < a2) { | |
blended = a1 * (1 - strength) + (a2 - 2 * Math.PI) * strength; | |
} else { | |
blended = a1 * (1 - strength) + (a2 + 2 * Math.PI) * strength; | |
} | |
blended = normalizeAngle(blended); | |
} | |
return blended; | |
} | |
function normalizeAngle(angle) { | |
while (angle < 0) angle += 2 * Math.PI; | |
while (angle >= 2 * Math.PI) angle -= 2 * Math.PI; | |
return angle; | |
} | |
function draw() { | |
// Clear canvas | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw background | |
ctx.fillStyle = '#F5DEB3'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
// Optimization: Only draw pheromones every 2nd frame when there are many | |
const skipPheromones = (pheromones.length + homePheromones.length) > 500 && frameCount % 2 === 0; | |
if (!skipPheromones) { | |
// Draw food pheromone trails | |
for (const p of pheromones) { | |
ctx.globalAlpha = p.life * 0.6; | |
ctx.fillStyle = 'rgba(255, 235, 59, 0.5)'; | |
ctx.beginPath(); | |
ctx.arc(p.x, p.y, config.pheromoneSize * (0.5 + p.life * 0.5), 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
// Draw home pheromone trails | |
for (const p of homePheromones) { | |
ctx.globalAlpha = p.life * 0.6; | |
ctx.fillStyle = 'rgba(255, 192, 203, 0.5)'; | |
ctx.beginPath(); | |
ctx.arc(p.x, p.y, config.pheromoneSize * (0.5 + p.life * 0.5), 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
} | |
ctx.globalAlpha = 1; | |
// Draw colony | |
ctx.fillStyle = '#5D4037'; | |
ctx.beginPath(); | |
ctx.arc(colony.x, colony.y, config.colonyRadius, 0, Math.PI * 2); | |
ctx.fill(); | |
// Draw colony entrance | |
ctx.fillStyle = '#8D6E63'; | |
ctx.beginPath(); | |
ctx.arc(colony.x, colony.y, config.colonyRadius * 0.7, 0, Math.PI * 2); | |
ctx.fill(); | |
// Draw food storage | |
ctx.fillStyle = '#FFFFFF'; | |
ctx.font = 'bold 14px Arial'; | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillText(colony.foodStored.toString(), colony.x, colony.y); | |
ctx.textAlign = 'left'; | |
ctx.textBaseline = 'alphabetic'; | |
// Draw food sources | |
for (const food of foodSources) { | |
const size = config.foodSize * (0.5 + (food.amount / config.foodCapacity) * 0.5); | |
// Draw food | |
ctx.fillStyle = '#4CAF50'; | |
ctx.beginPath(); | |
ctx.arc(food.x, food.y, size, 0, Math.PI * 2); | |
ctx.fill(); | |
// Draw food outline | |
ctx.strokeStyle = '#2E7D32'; | |
ctx.lineWidth = 1; | |
ctx.beginPath(); | |
ctx.arc(food.x, food.y, size, 0, Math.PI * 2); | |
ctx.stroke(); | |
// Draw food amount | |
ctx.fillStyle = '#FFFFFF'; | |
ctx.font = 'bold 10px Arial'; | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillText(Math.floor(food.amount).toString(), food.x, food.y); | |
ctx.textAlign = 'left'; | |
ctx.textBaseline = 'alphabetic'; | |
} | |
// Draw scouts first (so workers appear on top) | |
for (const scout of scouts) { | |
// Body | |
ctx.fillStyle = scout.carryingFood ? '#1565C0' : '#2196F3'; | |
ctx.beginPath(); | |
ctx.arc(scout.x, scout.y, scout.size, 0, Math.PI * 2); | |
ctx.fill(); | |
// Darker outline | |
ctx.strokeStyle = '#0D47A1'; | |
ctx.lineWidth = 1; | |
ctx.beginPath(); | |
ctx.arc(scout.x, scout.y, scout.size, 0, Math.PI * 2); | |
ctx.stroke(); | |
// Direction indicator | |
ctx.strokeStyle = '#000'; | |
ctx.lineWidth = 1; | |
ctx.beginPath(); | |
ctx.moveTo(scout.x, scout.y); | |
ctx.lineTo( | |
scout.x + Math.cos(scout.direction) * scout.size * 1.5, | |
scout.y + Math.sin(scout.direction) * scout.size * 1.5 | |
); | |
ctx.stroke(); | |
} | |
// Draw worker ants | |
for (const ant of ants) { | |
if (ant.type === 'queen') continue; | |
// Body | |
ctx.fillStyle = ant.carryingFood ? '#E91E63' : '#FF5722'; | |
ctx.beginPath(); | |
ctx.arc(ant.x, ant.y, ant.size * (ant.carryingFood ? 1.3 : 1), 0, Math.PI * 2); | |
ctx.fill(); | |
// Darker outline | |
ctx.strokeStyle = ant.carryingFood ? '#C2185B' : '#E64A19'; | |
ctx.lineWidth = 1; | |
ctx.beginPath(); | |
ctx.arc(ant.x, ant.y, ant.size * (ant.carryingFood ? 1.3 : 1), 0, Math.PI * 2); | |
ctx.stroke(); | |
// Direction indicator | |
ctx.strokeStyle = '#000'; | |
ctx.lineWidth = 1; | |
ctx.beginPath(); | |
ctx.moveTo(ant.x, ant.y); | |
ctx.lineTo( | |
ant.x + Math.cos(ant.direction) * ant.size * 1.5, | |
ant.y + Math.sin(ant.direction) * ant.size * 1.5 | |
); | |
ctx.stroke(); | |
} | |
// Draw queen last (so she's on top) | |
if (ants.length > 0 && ants[0].type === 'queen') { | |
const queen = ants[0]; | |
// Crown shape | |
ctx.fillStyle = '#9C27B0'; | |
ctx.beginPath(); | |
ctx.arc(queen.x, queen.y, queen.size, 0, Math.PI * 2); | |
ctx.fill(); | |
// Crown details | |
ctx.fillStyle = '#FFD700'; | |
const points = 5; | |
const innerRadius = queen.size * 0.6; | |
const outerRadius = queen.size * 1.4; | |
ctx.beginPath(); | |
for (let i = 0; i < points * 2; i++) { | |
const radius = i % 2 === 0 ? outerRadius : innerRadius; | |
const angle = (i * Math.PI / points) - Math.PI / 2; | |
const x = queen.x + Math.cos(angle) * radius; | |
const y = queen.y + Math.sin(angle) * radius; | |
if (i === 0) { | |
ctx.moveTo(x, y); | |
} else { | |
ctx.lineTo(x, y); | |
} | |
} | |
ctx.closePath(); | |
ctx.fill(); | |
// Darker outline | |
ctx.strokeStyle = '#7B1FA2'; | |
ctx.lineWidth = 1.5; | |
ctx.beginPath(); | |
ctx.arc(queen.x, queen.y, queen.size, 0, Math.PI * 2); | |
ctx.stroke(); | |
} | |
// Optional: Draw FPS (for debugging) | |
/* | |
if (fps > 0) { | |
ctx.fillStyle = '#000'; | |
ctx.font = '12px Arial'; | |
ctx.fillText(`FPS: ${Math.round(fps)}`, 10, 20); | |
} | |
*/ | |
frameCount++; | |
} | |
function animate(timestamp) { | |
if (!lastTime) lastTime = timestamp; | |
const deltaTime = timestamp - lastTime; | |
lastTime = timestamp; | |
// Calculate FPS | |
if (timestamp - lastFpsUpdate > 1000) { | |
fps = frameCount * 1000 / (timestamp - lastFpsUpdate); | |
frameCount = 0; | |
lastFpsUpdate = timestamp; | |
} | |
if (isRunning) { | |
for (let i = 0; i < simSpeed; i++) { | |
update(deltaTime / simSpeed); | |
} | |
draw(); | |
} | |
animationId = requestAnimationFrame(animate); | |
} | |
// Handle canvas click to add food | |
canvas.addEventListener('click', function(e) { | |
const rect = canvas.getBoundingClientRect(); | |
const scaleX = canvas.width / rect.width; | |
const scaleY = canvas.height / rect.height; | |
const x = (e.clientX - rect.left) * scaleX; | |
const y = (e.clientY - rect.top) * scaleY; | |
// Don't add food on top of colony | |
const distanceToColony = Math.hypot(x - colony.x, y - colony.y); | |
if (distanceToColony > config.colonyRadius + 20) { | |
addFoodAtPosition(x, y); | |
} | |
}); | |
// Initial draw | |
draw(); | |
}); | |
</script> | |
</body> | |
</html> |