Created
December 30, 2011 19:11
-
-
Save arctangent/1541077 to your computer and use it in GitHub Desktop.
Ant Simulation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Ant Foraging Simulator | |
// Jacob Conrad Martin | |
// http://jacobconradmartin.com | |
import processing.video.*; | |
MovieMaker mm; | |
// *** CONFIG *** | |
int worldWidth = 400; | |
int worldHeight = 400; | |
int foodPiles = 100; | |
int foodMaxDistance = min(worldWidth, worldHeight) / 2; | |
int foodMinDistance = 25; // Food will be at least this far away | |
int foodPerPile = 5; | |
int antReleaseRate = 10; // How fast the ants are released from the nest | |
int antCount = 0; | |
int maxAnts = 500; | |
int antLife = 1000; // How many turns each ant survives | |
int timeGearing = 500; // How many ants to move (chosen at random) before a screen refresh | |
// NOTE: Setting this higher than the total number of ants effectively means that complicated | |
// stuff like evaporation is calculated less often, therefore faster but less accurate simulation | |
int pMax = 100000; // Max pheremone allowed in a location | |
float pEvapRate = 0.1; // Evaporation rate | |
float pDiffRate = 0.1; // Diffusion rate | |
float neighbourK = 0.1; | |
int neighbourN = 10; | |
color cNest = color (0, 0, 0); | |
color cAnt = color (175, 0, 0); | |
color cAntWithFood = color(255, 0, 0); | |
color cImpassable = color(255, 255, 255); | |
color cFood = color(0, 125, 0); | |
int clock; | |
Location[][] world = new Location[worldWidth+2][worldHeight+2]; | |
Nest nest = new Nest(); | |
ArrayList ants; | |
// Directions | |
Direction dirN = new Direction(0,1); | |
Direction dirNE = new Direction(1,1); | |
Direction dirE = new Direction(1,0); | |
Direction dirSE = new Direction(1,-1); | |
Direction dirS = new Direction(0,-1); | |
Direction dirSW = new Direction(-1,-1); | |
Direction dirW = new Direction(-1,0); | |
Direction dirNW = new Direction(-1,1); | |
Direction[] allDirections = {dirN, dirNE, dirE, dirSE, dirS, dirSW, dirW, dirNW}; | |
void setup() { | |
clock = 0; | |
antCount = 0; | |
background(0); | |
size(worldWidth+2, worldHeight+2, P2D); // NB: 1px border will be drawn around edge | |
frameRate(30); | |
int moviename = (int)random(1,100000); | |
mm = new MovieMaker(this, width, height, "drawing"+str(moviename)+".mov", 24, MovieMaker.MOTION_JPEG_A, MovieMaker.LOSSLESS); | |
//mm = new MovieMaker(this, width, height, "drawing"+str(moviename)+".mov", 30, MovieMaker.ANIMATION, MovieMaker.LOW); | |
// init storage space for ants | |
ants = new ArrayList(); | |
// Init world | |
for (int wx=0; wx<worldWidth+2; wx++) { | |
for (int wy=0; wy<worldHeight+2; wy++) { | |
world[wx][wy] = new Location(); | |
if ( (wx==0) || (wx==worldWidth+1) || (wy==0) || (wy==worldHeight+1) ) { | |
world[wx][wy].isImpassable = true; | |
} | |
} | |
} | |
// Init food | |
for (int f=0; f<foodPiles; f++) { | |
int fx = 0; | |
int fy = 0; | |
boolean valid = false; | |
while (!valid) { | |
fx = (int)random(worldWidth); | |
fy = (int)random(worldHeight); | |
float dist = sqrt(pow(fx-nest.x,2)+pow(fy-nest.y,2)); | |
if ( (dist<foodMaxDistance) && (dist>foodMinDistance) ) { | |
world[fx][fy].foodCount=foodPerPile; | |
valid = true; | |
} | |
} | |
} | |
} | |
void draw() { | |
clock++; | |
background(0); // THIS IS VERY SLOW... | |
nest.releaseAnts(); | |
//drawEdges(); | |
diffusePheremone(); | |
evaporatePheremone(); | |
drawPheremone(); | |
drawAnts(); | |
drawFood(); | |
drawNest(); | |
for (int i = 0; i<ants.size()*timeGearing; i++) { | |
int a = (int)random(ants.size()); | |
Ant ant = (Ant)ants.get(a); | |
ant.move(); | |
} | |
// Draw pretty numbers on the screen | |
/* | |
PFont font; | |
font = loadFont("Helvetica-12.vlw"); | |
textFont(font); | |
text("T: "+clock, 20, 20); | |
text("F: "+nest.foodCollected, 80, 20); | |
int fitness = (int)(1000 * nest.foodCollected/clock); | |
text("FIT: " + fitness, 140, 20); | |
*/ | |
mm.addFrame(); // Add window's pixels to movie | |
} | |
// *** OBJECTS *** | |
class Direction { | |
int x, y; | |
Direction(int dx, int dy) { | |
x = dx; | |
y = dy; | |
} | |
boolean equals(Direction d) { | |
return ( (x==d.x) && (y==d.y) ); | |
} | |
} | |
class Location { | |
int foodCount = 0; | |
float pHomeCount = 0; | |
float pFoodCount = 0; | |
boolean isImpassable = false; | |
} | |
class Nest { | |
int x, y; | |
int foodCollected = 0; | |
Nest() { | |
x = (int)(worldWidth/2) + 1; | |
y = (int)(worldHeight/2) + 1; | |
} | |
void releaseAnts() { | |
for (int a=0; a<antReleaseRate; a++) { | |
if (antCount < maxAnts) { | |
ants.add(new Ant()); | |
antCount++; | |
} | |
} | |
} | |
} | |
class Ant { | |
int x, y; | |
int life = antLife; | |
Direction orientation = new Direction(0,0); | |
boolean hasFood = false; | |
String guid = ""; | |
Ant() { | |
x = nest.x; | |
y = nest.y; | |
hasFood = false; | |
orientation.x = (int)random(-2,2); | |
orientation.y = (int)random(-2,2); | |
guid = str(random(999999999)); | |
} | |
boolean atNest() { | |
return ( (x==nest.x) && (y==nest.y) ); | |
} | |
boolean atFood() { | |
return (world[x][y].foodCount>0); | |
} | |
void move() { | |
// Pick up food and drop pheremones where necessary | |
if (hasFood) { | |
if (atNest()) { | |
hasFood=false; | |
world[x][y].pHomeCount=pMax; | |
spin180(); | |
findFood(); | |
nest.foodCollected++; | |
// Did we collect 95% of the food? | |
if (nest.foodCollected > 0.95*foodPiles*foodPerPile) { | |
mm.addFrame(); | |
exit(); | |
} | |
} else { | |
world[x][y].pFoodCount += (pMax*life/antLife); | |
world[x][y].pFoodCount = min(world[x][y].pFoodCount, pMax); | |
findNest(); | |
} | |
} else { | |
if (atFood()) { | |
world[x][y].foodCount--; | |
hasFood=true; | |
spin180(); | |
world[x][y].pFoodCount=pMax; | |
findNest(); | |
} else if (atNest()) { | |
world[x][y].pHomeCount=pMax; | |
findFood(); | |
} else { | |
world[x][y].pHomeCount += (pMax*life/antLife); | |
world[x][y].pHomeCount = min(world[x][y].pHomeCount, pMax); | |
findFood(); | |
} | |
} | |
// Check if the ant is dead yet | |
life--; | |
if (life==0) { | |
for (int a=0; a<ants.size(); a++) { // NOTE: Very slow way to remove this ant from the ants ArrayList... | |
Ant ant = (Ant)ants.get(a); | |
if (ant.guid==guid) { | |
if ((ant.hasFood) && (!atNest())) { | |
world[ant.x][ant.y].foodCount++; | |
} | |
ants.remove(a); | |
antCount--; | |
break; | |
} | |
} | |
} | |
} | |
void findNest() { | |
findThing("nest"); | |
} | |
void findFood() { | |
findThing("food"); | |
} | |
Direction[] forwardDirections() { | |
if (orientation.equals(dirN)) { | |
Direction[] dirs = {dirNW, dirN, dirNE}; | |
return dirs; | |
} else if (orientation.equals(dirNE)) { | |
Direction[] dirs = {dirN, dirNE, dirE}; | |
return dirs; | |
} else if (orientation.equals(dirE)) { | |
Direction[] dirs = {dirNE, dirE, dirSE}; | |
return dirs; | |
} else if (orientation.equals(dirSE)) { | |
Direction[] dirs = {dirE, dirSE, dirS}; | |
return dirs; | |
} else if (orientation.equals(dirS)) { | |
Direction[] dirs = {dirSE, dirS, dirSW}; | |
return dirs; | |
} else if (orientation.equals(dirSW)) { | |
Direction[] dirs = {dirS, dirSW, dirW}; | |
return dirs; | |
} else if (orientation.equals(dirW)) { | |
Direction[] dirs = {dirSW, dirW, dirNW}; | |
return dirs; | |
} else if (orientation.equals(dirNW)) { | |
Direction[] dirs = {dirW, dirNW, dirN}; | |
return dirs; | |
} else { | |
return allDirections; | |
} | |
} | |
void spin180() { | |
orientation.x = -1 * orientation.x; | |
orientation.y = -1 * orientation.y; | |
} | |
void findThing(String thing) { | |
Direction[] directions = forwardDirections(); | |
int directionsCount = directions.length; | |
// Weightings | |
float[] w = new float[directionsCount]; | |
for (int i=0; i<directionsCount; i++) { | |
if (world[x+directions[i].x][y+directions[i].y].isImpassable==false) { | |
if (thing == "nest") { | |
w[i] = pow(neighbourK + world[x+directions[i].x][y+directions[i].y].pHomeCount/pMax, neighbourN); | |
} else if (thing == "food") { | |
w[i] = pow(neighbourK + world[x+directions[i].x][y+directions[i].y].pFoodCount/pMax, neighbourN); | |
} | |
} | |
} | |
// Cumulative weightings | |
float[] wcum = new float[directionsCount]; | |
wcum[0] = w[0]; | |
for (int i=1; i<directionsCount; i++) { | |
wcum[i] = wcum[i-1] + w[i]; | |
} | |
// Calculate the direction and move there | |
float r = (float)random(0, wcum[directionsCount-1]); | |
for (int j=0; j<directionsCount; j++) { | |
if ( r < wcum[j]) { | |
int axNew = x+directions[j].x; | |
int ayNew = y+directions[j].y; | |
if (world[axNew][ayNew].isImpassable==false) { | |
x = axNew; | |
y = ayNew; | |
orientation.x = directions[j].x; | |
orientation.y = directions[j].y; | |
} | |
break; | |
} | |
// If we reach this point then the ant had no legal move | |
spin180(); | |
} | |
} | |
} | |
// *** HELPER FUNCTIONS *** | |
void drawEdges() { | |
stroke(cImpassable); | |
line(0, 0, 0, worldHeight+1); | |
line(0, 0, worldWidth+1, 0); | |
line(worldWidth+1, 0, worldWidth+1, worldHeight+1); | |
line(0, worldHeight+1, worldHeight+1, worldWidth+1); | |
} | |
void drawAnts() { | |
for (int a = 0; a < ants.size(); a++) { | |
Ant ant = (Ant)ants.get(a); | |
if (ant.hasFood) { | |
fill(cAntWithFood); | |
stroke(cAntWithFood); | |
rect(ant.x, ant.y, 1, 1); | |
fill(255); | |
stroke(255); | |
} else { | |
fill(cAnt); | |
stroke(cAnt); | |
rect(ant.x, ant.y, 1, 1); | |
fill(255); | |
stroke(255); | |
} | |
} | |
} | |
void drawFood() { | |
for (int y = 0; y < worldHeight+2; y++) { | |
for (int x = 0; x < worldWidth+2; x++) { | |
if (world[x][y].foodCount > 0) { | |
fill(cFood); | |
stroke(cFood); | |
if (world[x][y].foodCount > 0.75 * foodPerPile) { | |
rect(x, y, 3, 3); | |
} else if (world[x][y].foodCount > 0.5 * foodPerPile) { | |
rect(x, y, 2, 2); | |
} else { | |
rect(x, y, 1, 1); | |
} | |
fill(255); | |
stroke(255); | |
} | |
} | |
} | |
} | |
void drawNest() { | |
//set(nest.x, nest.y, cNest); | |
fill(cNest); | |
stroke(cNest); | |
ellipse(nest.x, nest.y, 5, 5); | |
fill(255); | |
stroke(255); | |
} | |
void diffusePheremone() { | |
int[][] pFoodTransfer = new int[worldWidth+2][worldHeight+2]; | |
int[][] pHomeTransfer = new int[worldWidth+2][worldHeight+2]; | |
// Calculate the transfer matrix | |
for (int y = 2; y < worldHeight; y++) { | |
for (int x = 2; x < worldWidth; x++) { | |
for (int k = -1; k < 2; k++) { | |
for (int l = -1; l < 2; l++) { | |
pFoodTransfer[x+k][y+l] += (int)((world[x][y].pFoodCount - world[x+k][y+l].pFoodCount) * pDiffRate); | |
pHomeTransfer[x+k][y+l] += (int)((world[x][y].pHomeCount - world[x+k][y+l].pHomeCount) * pDiffRate); | |
} | |
} | |
} | |
} | |
// Apply the transfer matrix | |
for (int y = 1; y < worldHeight+1; y++) { | |
for (int x = 1; x < worldWidth+1; x++) { | |
world[x][y].pFoodCount += pFoodTransfer[x][y]; | |
world[x][y].pHomeCount += pHomeTransfer[x][y]; | |
} | |
} | |
} | |
void evaporatePheremone() { | |
for (int y = 0; y < worldHeight+2; y++) { | |
for (int x = 0; x < worldWidth+2; x++) { | |
world[x][y].pHomeCount = (int)(world[x][y].pHomeCount * (1-pEvapRate)); | |
world[x][y].pFoodCount = (int)(world[x][y].pFoodCount * (1-pEvapRate)); | |
} | |
} | |
} | |
void drawPheremone() { | |
for (int y = 0; y < worldHeight+2; y++) { | |
for (int x = 0; x < worldWidth+2; x++) { | |
if (world[x][y].pFoodCount + world[x][y].pHomeCount > 0) { | |
int f = (int)(255*world[x][y].pFoodCount/pMax); | |
int h = (int)(255*world[x][y].pHomeCount/pMax); | |
set(x, y, color(0, f, h)); | |
} | |
} | |
} | |
} | |
// *** INTERACTIVITY *** | |
void keyPressed() { | |
//mm.addFrame(); // Add window's pixels to movie | |
mm.finish(); | |
exit(); | |
} | |
void mouseClicked() { | |
mm.finish(); | |
nest.foodCollected = 0; | |
setup(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment