ant / index.html
kimhyunwoo's picture
Update index.html
2e8d84e verified
<!DOCTYPE html>
<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>