Skip to content

Commit

Permalink
Initial angle steering
Browse files Browse the repository at this point in the history
  • Loading branch information
hasnainroopawalla authored Aug 30, 2023
1 parent a2eb6ff commit d3d99a6
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 74 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ant-colony-simulation",
"version": "0.0.5",
"version": "0.0.4",
"description": "Ant colony simulation with TypeScript and p5.js",
"homepage": "https://hasnainroopawalla.github.io/ant-colony-simulation",
"private": true,
Expand Down Expand Up @@ -39,4 +39,4 @@
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
}
112 changes: 61 additions & 51 deletions src/aco/ant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { World } from "./world";
import { Colony } from "./colony";
import { config } from "./config";
import { IPheromoneType, Pheromone } from "./pheromone";
import { areCirclesOverlapping, distance, randomFloat } from "./utils";
import { areCirclesIntersecting, distance, randomFloat } from "./utils";
import { Vector, fromAngle } from "./vector";

export enum IAntState {
Expand All @@ -16,6 +16,7 @@ export class Ant {
world: World;
position: Vector;
velocity: Vector;
desiredVelocity: Vector;
acceleration: Vector;
angle: number;
wanderAngle: number;
Expand All @@ -32,19 +33,18 @@ export class Ant {
this.wanderAngle = 0;
this.angle = randomFloat(0, this.p.TWO_PI);
this.velocity = fromAngle(this.angle);
this.desiredVelocity = this.velocity.copy();
this.acceleration = new Vector();
this.searchingForFood();
}

private approachTarget(target: Vector) {
const speedControl = target
.sub(this.position)
.setMagnitude(config.antMaxSpeed);
return speedControl.sub(this.velocity).limit(config.antSteeringLimit);
}

private applyForce(force: Vector) {
this.acceleration.add(force, true);
private approachTarget(target: Vector): void {
// TODO: modify
const bearing = new Vector(
target.x - this.position.x,
target.y - this.position.y
);
this.desiredVelocity = bearing.normalize();
}

private getAntennas(): {
Expand All @@ -68,66 +68,76 @@ export class Ant {
return { leftAntenna, forwardAntenna, rightAntenna };
}

private handleEdgeCollision() {
// left / right
if (this.position.x > this.p.windowWidth - 10 || this.position.x < 10) {
this.velocity.x *= -1;
}
// top / bottom
if (this.position.y > this.p.windowHeight - 10 || this.position.y < 10) {
this.velocity.y *= -1;
}
// TODO: ants should not be rendered over colonies
private getPerception(): Vector {
return this.position.add(this.velocity.mult(config.antPerceptionRange));
}

private handleObstacles(): void {
let obstacleInRange: boolean;
do {
const perception = this.position.add(
this.desiredVelocity.mult(config.antPerceptionRange * 2)
);
obstacleInRange = this.world.isObstacleInAntPerceptionRange(
this.position,
perception
);
if (obstacleInRange) {
// randomly set positive or negative angleRange
// TODO: turn left/right based on the angle of collision
this.desiredVelocity.rotate(
Math.random() < 0.5
? config.antObstacleAngleRange
: -config.antObstacleAngleRange,
true
);
}
} while (obstacleInRange);
}

private handleWandering() {
this.wanderAngle += randomFloat(-0.5, 0.5);
const circlePos = this.velocity
.setMagnitude(config.antPerceptionRange)
.add(this.position);
const circleOffset = fromAngle(
this.wanderAngle + this.velocity.heading()
).mult(config.antWanderStrength);
const target = circlePos.add(circleOffset);
const wander = this.approachTarget(target);
this.applyForce(wander);
const angle = randomFloat(-1, 1);
this.desiredVelocity.rotate(angle * config.antWanderStrength, true);
}

private handleAntennaSteering(pheromoneType: IPheromoneType) {
const antennas = this.getAntennas();
const [leftAntenna, frontAntenna, rightAntenna] =
this.world.antennaPheromoneValues(
this.world.computeAntAntennasPheromoneValues(
[antennas.leftAntenna, antennas.forwardAntenna, antennas.rightAntenna],
config.antAntennaRadius,
pheromoneType
);

if (frontAntenna > leftAntenna && frontAntenna > rightAntenna) {
// do nothing
} else if (leftAntenna > rightAntenna) {
this.applyForce(this.approachTarget(antennas.leftAntenna));
this.desiredVelocity.rotate(-2.35 / 2);
} else if (rightAntenna > leftAntenna) {
this.applyForce(this.approachTarget(antennas.rightAntenna));
this.desiredVelocity.rotate(2.35 / 2);
}
}

private handleSearchingForFood() {
// check if food item exists within perception range
if (!this.targetFoodItem) {
this.targetFoodItem = this.world.getFoodItemInPerceptionRange(
this.position
this.targetFoodItem = this.world.getFoodItemInAntPerceptionRange(
this.getPerception(),
config.antPerceptionRange
);
}

if (this.targetFoodItem) {
// check if reserved food item is picked up
if (this.targetFoodItem.collide(this.position)) {
// rotate 180 degrees
this.velocity.rotate(this.p.PI, true);
this.desiredVelocity.rotate(Math.PI, true);
this.targetFoodItem.pickedUp();
this.returningHome();
} else {
// TODO: improve if-else condition
this.approachTarget(this.targetFoodItem.position);
}
const approachFood = this.approachTarget(this.targetFoodItem.position);
this.applyForce(approachFood);
} else {
// follow food pheromones if no food item is found within perception range
this.handleAntennaSteering(IPheromoneType.Food);
Expand All @@ -136,13 +146,13 @@ export class Ant {

private handleReturningHome() {
if (this.colonyInPerceptionRange()) {
const approachColony = this.approachTarget(this.colony.position);
this.applyForce(approachColony);
this.approachTarget(this.colony.position);
// this.applyForce(approachColony);

// check if food item is delivered to colony
if (this.colony.collide(this.position)) {
// rotate 180 degrees
this.velocity.rotate(this.p.PI, true);
// this.velocity.rotate(Math.PI, true);
this.targetFoodItem.delivered();
this.targetFoodItem = null;
this.colony.incrementFoodCount();
Expand All @@ -156,7 +166,7 @@ export class Ant {
}

private colonyInPerceptionRange(): boolean {
return areCirclesOverlapping(
return areCirclesIntersecting(
this.position,
config.antPerceptionRange * 2,
this.colony.position,
Expand Down Expand Up @@ -187,9 +197,12 @@ export class Ant {
}

private updatePosition() {
this.velocity.add(this.acceleration, true);
this.position.add(this.velocity, true);
this.acceleration.set(0);
const subtracted = this.desiredVelocity.sub(this.velocity);
const desiredSteer = subtracted.mult(config.antSteeringLimit);
const acceleration = desiredSteer.limit(config.antSteeringLimit);

this.velocity.add(acceleration, true).limit(config.antMaxSpeed);
this.position.add(this.velocity.mult(config.antMaxSpeed), true);
}

// TODO: Create a wrapper for render methods to handle push/pop logic
Expand All @@ -216,11 +229,8 @@ export class Ant {
this.p.push();
this.p.strokeWeight(config.antPerceptionStrokeWeight);
this.p.fill(config.antPerceptionColorGray, config.antPerceptionColorAlpha);
this.p.circle(
this.position.x,
this.position.y,
config.antPerceptionRange * 2
);
const perception = this.getPerception();
this.p.circle(perception.x, perception.y, config.antPerceptionRange * 2);
this.p.pop();
}

Expand All @@ -241,7 +251,7 @@ export class Ant {
}

public update() {
this.handleEdgeCollision();
this.handleObstacles();
this.handlePheromoneDeposit();

this.isSearchingForFood() && this.handleSearchingForFood();
Expand Down
15 changes: 8 additions & 7 deletions src/aco/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,32 @@ export const config = {
// world
worldBackground: "#78624f",
// food item
foodItemSize: 2, // diameter
foodItemSize: 4, // diameter
foodItemColor: "#39FF14",
foodItemStrokeWeight: 0,
// food cluster
foodClusterSpacing: 7,
foodClusterSpacing: 10,
// colony
colonySize: 70, // diameter
colonyColor: "#ffffff",
colonyStrokeWeight: 1,
colonyTextSize: 20,
antWanderStrength: 1.5,
antMaxSpeed: 2.5,
antWanderStrength: 0.2,
antMaxSpeed: 2,
antSteeringLimit: 0.3,
antSize: 1,
antSize: 4,
antColor: "#000000",
antStrokeWeight: 2,
showAntPerceptionRange: false,
antPerceptionRange: 50, //radius
antPerceptionRange: 25, //radius
antPerceptionColorGray: 255,
antPerceptionColorAlpha: 30,
antPerceptionStrokeWeight: 1,
showAntAntenna: false,
antAntennaRadius: 20,
antObstacleAngleRange: 0.7,
// pheromone
pheromoneSize: 2,
pheromoneSize: 4,
pheromoneStrokeWeight: 0,
pheromoneDistanceBetween: 400,
pheromoneEvaporationRate: 1,
Expand Down
1 change: 1 addition & 0 deletions src/aco/obstacle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Obstacle {}
10 changes: 6 additions & 4 deletions src/aco/quadtree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,11 @@ export class Quadtree<T extends IQuadtree> {
}

private updateAndRenderPoints() {
this.points.map((point) => {
for (let i = 0; i < this.points.length; i++) {
const point = this.points[i];
point.update();
point.render();
});
}
}

private highlightQuadtree() {
Expand Down Expand Up @@ -203,11 +204,12 @@ export class Quadtree<T extends IQuadtree> {
// Highlight the quadtrees in the perception range of the ant
config.showHighlightedQuadtree && this.highlightQuadtree();

this.points.map((point) => {
for (let i = 0; i < this.points.length; i++) {
const point = this.points[i];
if (range.contains(point)) {
found.push(point);
}
});
}

if (this.divided) {
this.topLeft.query(range, found);
Expand Down
6 changes: 5 additions & 1 deletion src/aco/sketch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IConfig } from "./sketch.interface";

let world: World;
let canvasInteractionEnabled = true;
const numAnts: number = 200;
const numAnts: number = 100;

export const updateAcoConfig = <T extends keyof IConfig>(
param: T,
Expand All @@ -27,6 +27,10 @@ export const sketch = (p: p5) => {
world.render();
};

p.keyPressed = () => {
world.loop = false;
};

p.mousePressed = () => {
if (!canvasInteractionEnabled) {
return;
Expand Down
20 changes: 19 additions & 1 deletion src/aco/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function pointInCircle(
);
}

export function areCirclesOverlapping(
export function areCirclesIntersecting(
circle1Position: Vector,
circle1Radius: number,
circle2Position: Vector,
Expand All @@ -42,3 +42,21 @@ export function areCirclesOverlapping(
export function randomFloat(min: number, max: number): number {
return Math.random() * (max - min) + min;
}

// TODO: refactor
export function areLinesIntersecting(
line1: { x1: number; y1: number; x2: number; y2: number },
line2: { x1: number; y1: number; x2: number; y2: number }
): boolean {
const uA =
((line2.x2 - line2.x1) * (line1.y1 - line2.y1) -
(line2.y2 - line2.y1) * (line1.x1 - line2.x1)) /
((line2.y2 - line2.y1) * (line1.x2 - line1.x1) -
(line2.x2 - line2.x1) * (line1.y2 - line1.y1));
const uB =
((line1.x2 - line1.x1) * (line1.y1 - line2.y1) -
(line1.y2 - line1.y1) * (line1.x1 - line2.x1)) /
((line2.y2 - line2.y1) * (line1.x2 - line1.x1) -
(line2.x2 - line2.x1) * (line1.y2 - line1.y1));
return uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1;
}
6 changes: 6 additions & 0 deletions src/aco/vector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export class Vector {
return Math.atan2(this.y, this.x);
}

public normalize(assign?: boolean): Vector {
const result = this.mult(1 / this.getMagnitude());
assign && this.assign(result);
return result;
}

public copy(): Vector {
return new Vector(this.x, this.y);
}
Expand Down
Loading

0 comments on commit d3d99a6

Please sign in to comment.