diff --git a/package.json b/package.json index 226572e..6647562 100644 --- a/package.json +++ b/package.json @@ -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, @@ -39,4 +39,4 @@ "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" } -} +} \ No newline at end of file diff --git a/src/aco/ant.ts b/src/aco/ant.ts index 4a67340..d8b3877 100644 --- a/src/aco/ant.ts +++ b/src/aco/ant.ts @@ -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 { @@ -16,6 +16,7 @@ export class Ant { world: World; position: Vector; velocity: Vector; + desiredVelocity: Vector; acceleration: Vector; angle: number; wanderAngle: number; @@ -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(): { @@ -68,53 +68,62 @@ 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 ); } @@ -122,12 +131,13 @@ export class Ant { // 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); @@ -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(); @@ -156,7 +166,7 @@ export class Ant { } private colonyInPerceptionRange(): boolean { - return areCirclesOverlapping( + return areCirclesIntersecting( this.position, config.antPerceptionRange * 2, this.colony.position, @@ -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 @@ -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(); } @@ -241,7 +251,7 @@ export class Ant { } public update() { - this.handleEdgeCollision(); + this.handleObstacles(); this.handlePheromoneDeposit(); this.isSearchingForFood() && this.handleSearchingForFood(); diff --git a/src/aco/config.ts b/src/aco/config.ts index 38d7272..6959ff5 100644 --- a/src/aco/config.ts +++ b/src/aco/config.ts @@ -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, diff --git a/src/aco/obstacle.ts b/src/aco/obstacle.ts new file mode 100644 index 0000000..999342c --- /dev/null +++ b/src/aco/obstacle.ts @@ -0,0 +1 @@ +class Obstacle {} diff --git a/src/aco/quadtree.ts b/src/aco/quadtree.ts index 4e884e8..abfdc71 100644 --- a/src/aco/quadtree.ts +++ b/src/aco/quadtree.ts @@ -145,10 +145,11 @@ export class Quadtree { } 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() { @@ -203,11 +204,12 @@ export class Quadtree { // 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); diff --git a/src/aco/sketch.ts b/src/aco/sketch.ts index 9d6b6a3..bbb7152 100644 --- a/src/aco/sketch.ts +++ b/src/aco/sketch.ts @@ -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 = ( param: T, @@ -27,6 +27,10 @@ export const sketch = (p: p5) => { world.render(); }; + p.keyPressed = () => { + world.loop = false; + }; + p.mousePressed = () => { if (!canvasInteractionEnabled) { return; diff --git a/src/aco/utils.ts b/src/aco/utils.ts index 06b44c5..0ff3a81 100644 --- a/src/aco/utils.ts +++ b/src/aco/utils.ts @@ -26,7 +26,7 @@ export function pointInCircle( ); } -export function areCirclesOverlapping( +export function areCirclesIntersecting( circle1Position: Vector, circle1Radius: number, circle2Position: Vector, @@ -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; +} diff --git a/src/aco/vector.ts b/src/aco/vector.ts index 3caabe3..074fd87 100644 --- a/src/aco/vector.ts +++ b/src/aco/vector.ts @@ -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); } diff --git a/src/aco/world.ts b/src/aco/world.ts index f3dc446..8fd36d5 100644 --- a/src/aco/world.ts +++ b/src/aco/world.ts @@ -5,14 +5,17 @@ import { config } from "./config"; import { IPheromoneType, Pheromone } from "./pheromone"; import { quadtreeCircle, Quadtree, Rectangle } from "./quadtree"; import { Vector } from "./vector"; +import { areLinesIntersecting } from "./utils"; export class World { p: p5; + loop: boolean = true; foodItemsQuadtree: Quadtree; homePheromoneQuadtree: Quadtree; foodPheromoneQuadtree: Quadtree; ants: Ant[]; colonies: Colony[]; + obstacles: any[]; constructor(p: p5) { this.p = p; @@ -45,6 +48,22 @@ export class World { ); this.ants = []; this.colonies = [new Colony(this.p)]; + this.obstacles = [ + { x1: 0, y1: 0, x2: this.p.windowWidth, y2: 0 }, + { + x1: 0, + y1: this.p.windowHeight, + x2: this.p.windowWidth, + y2: this.p.windowHeight, + }, + { x1: 0, y1: 0, x2: 0, y2: this.p.windowHeight }, + { + x1: this.p.windowWidth, + y1: 0, + x2: this.p.windowWidth, + y2: this.p.windowHeight, + }, + ]; } public createAnt() { @@ -74,8 +93,11 @@ export class World { } // TODO: this method should limit the perception to only in FRONT of the ant - public getFoodItemInPerceptionRange(antPosition: Vector): FoodItem | null { - quadtreeCircle.set(antPosition.x, antPosition.y, config.antPerceptionRange); + public getFoodItemInAntPerceptionRange( + antPosition: Vector, + antPerceptionRange: number + ): FoodItem | null { + quadtreeCircle.set(antPosition.x, antPosition.y, antPerceptionRange); const found = this.foodItemsQuadtree.query(quadtreeCircle); for (let i = 0; i < found.length; i++) { const foodItem = found[i]; @@ -86,8 +108,10 @@ export class World { } } - public antennaPheromoneValues( + // TODO: optimize this method + public computeAntAntennasPheromoneValues( antennas: Vector[], + antAntennaRadius: number, pheromoneType: IPheromoneType ): number[] { let antennaScores: number[] = []; @@ -99,17 +123,40 @@ export class World { for (let i = 0; i < antennas.length; i++) { const antenna = antennas[i]; let antennaScore = 0; - quadtreeCircle.set(antenna.x, antenna.y, config.antAntennaRadius); + quadtreeCircle.set(antenna.x, antenna.y, antAntennaRadius); const pheromones = pheromoneQuadtree.query(quadtreeCircle); - pheromones.map((pheromone) => { + for (let j = 0; j < pheromones.length; j++) { + const pheromone = pheromones[j]; antennaScore += pheromone.strength; - }); + } antennaScores.push(antennaScore); } - return antennaScores; } + public isObstacleInAntPerceptionRange( + antPosition: Vector, + antPerception: Vector + ): boolean { + for (let i = 0; i < this.obstacles.length; i++) { + const obstacle = this.obstacles[i]; + if ( + areLinesIntersecting( + { + x1: antPosition.x, + y1: antPosition.y, + x2: antPerception.x, + y2: antPerception.y, + }, + { x1: obstacle.x1, y1: obstacle.y1, x2: obstacle.x2, y2: obstacle.y2 } + ) + ) { + return true; + } + } + return false; + } + private updateAndRenderAnts() { for (let i = 0; i < this.ants.length; i++) { const ant = this.ants[i]; @@ -126,7 +173,8 @@ export class World { } private updateAndRenderQuadtrees() { - this.foodItemsQuadtree.updateAndRender(config.showFoodItemsQuadtree); + this.loop && + this.foodItemsQuadtree.updateAndRender(config.showFoodItemsQuadtree); this.homePheromoneQuadtree.updateAndRender( config.showHomePheromonesQuadtree );