A Simulation of Pest Eradication in JavaScript

The JavaScript code below, when run, simulates a ecological pest-eradication scenario. An exterminator chooses a target at random, travels over a network of links and nodes to get as close as possible to the target, moves off the network towards the target itself, and removes it. The exterminator then chooses another target and random and continues.

The simulation was originally intended to test the feasibility of removing invasive or otherwise unwanted species such as poison ivy or fire ants. It is anticipated that the simulation will need a significant number of enhancements to effectively model such scenarios, including but not limited to: non-random selection of the next target, initial concealment of targets that must somehow be detected, propagation of targets over time, permanent “reservoirs” from which targets cannot be removed, and accounting for the time and resource costs of travel and eradication.

<html>
<body>
<div id="divMain"></div>
<script type="text/javascript">

// main

function main()
{
	var map = Map.random
	(
		new Coords(300, 300), // size
		new Coords(10, 10), // margin
		200 // numberOfNodes
	);

	var world = World.random
	(
		map, 
		1, // numberOfAgents
		100 // numberOfTargets
	);

	var display = new Display(map.size);

	Globals.Instance.initialize(display, world);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}
		return this;
	}
}

// classes

function Agent(id, pos)
{
	this.id = id;
	this.pos = pos;

	this.target = null;
	this.route = new Route([]);

	this.speed = 1;
}
{
	Agent.prototype.act = function(world)
	{
		var map = world.map;
		var mapNodes = map.nodes;
		var routeNodeIDs = this.route.nodeIDs;
		if (this.target == null)
		{
			var nodeStart = map.nodeNearestToPos(this.pos);
			var targetIndex = Math.floor
			(
				Math.random() * world.targets.length
			);
			this.target = world.targets[targetIndex];
			var nodeToTarget = map.nodeNearestToPos(this.target.pos);
			this.route.fromNodeIDToNodeID
			(
				map, nodeStart.id, nodeToTarget.id
			);
		}
		else
		{
			var targetPos;
			if (routeNodeIDs.length == 0)
			{
				targetPos = this.target.pos;
			}
			else
			{
				var idOfNodeNext = routeNodeIDs[0];
				var nodeNext = mapNodes[idOfNodeNext];
				targetPos = nodeNext.pos;
			}

			var displacementToTarget = targetPos.clone().subtract
			(
				this.pos
			);
			var distanceToTarget = displacementToTarget.magnitude();
			if (distanceToTarget < this.speed)
			{
				this.pos.overwriteWith(targetPos);
				if (routeNodeIDs.length == 0)
				{
					var targetIndex = world.targets.indexOf(this.target);
					world.targets.splice(targetIndex, 1);
					delete world.targets[this.target.id];
					this.target = null;
				}
				else
				{
					routeNodeIDs.splice(0, 1);
				}
			}
			else
			{
				var movement = displacementToTarget.divideScalar
				(
					distanceToTarget
				).multiplyScalar
				(
					this.speed	
				);
	
				this.pos.add(movement);
			}
		}
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}

	Coords.prototype.random = function()
	{
		this.x = Math.random();
		this.y = Math.random();
		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}
}

function Display(size)
{
	this.size = size;
}
{
	Display.prototype.clear = function()
	{
		this.graphics.fillRect(0, 0, this.size.x, this.size.y);
		this.graphics.strokeRect(0, 0, this.size.x, this.size.y);
	}

	Display.prototype.colorBack = function(value)
	{
		this.graphics.fillStyle = value;
	}

	Display.prototype.colorFore = function(value)
	{
		this.graphics.strokeStyle = value;
	}

	Display.prototype.drawCircle = function(center, radius)
	{
		this.graphics.beginPath();
		this.graphics.arc
		(
			center.x, center.y,
			radius,
			0, Math.PI * 2 // start and stop angles
		);
		this.graphics.stroke();
	}

	Display.prototype.drawLine = function(fromPos, toPos)
	{
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}

	Display.prototype.drawRectangle = function(center, size)
	{
		this.graphics.strokeRect
		(
			center.x - size.x / 2, center.y - size.y / 2, 
			size.x, size.y
		);
	}

	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;

		this.graphics = canvas.getContext("2d");

		this.colorFore("Gray");
		this.colorBack("White");

		var divMain = document.getElementById("divMain");
		divMain.appendChild(canvas);

		return this;
	}
}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(display, world)
	{
		this.display = display.initialize();
		this.world = world;

		var timerTicksPerSecond = 10;
		var millisecondsPerTimerTick = Math.floor
		(
			1000 / timerTicksPerSecond
		);
		this.timer = setInterval
		(
			this.updateForTimerTick.bind(this), 
			millisecondsPerTimerTick
		);
	}

	Globals.prototype.updateForTimerTick = function()
	{		
		this.display.clear();
		this.world.drawToDisplay(this.display);

		this.world.updateForTimerTick();
	}
}

function IDHelper()
{
	this._idNext = 0;
}
{
	IDHelper.idNext = function()
	{
		var returnValue = "_" + this._idNext;
		this._idNext++;
		return returnValue;
	}
}

function Map(size, nodes, links)
{
	this.size = size;
	this.nodes = nodes.addLookups("id");
	this.links = links.addLookups("id");

	for (var i = 0; i < this.links.length; i++)
	{
		var link = this.links[i];
		var linkNodeIDs = link.nodeIDs;

		var nodeID0 = linkNodeIDs[0];
		var nodeID1 = linkNodeIDs[1];

		var node0 = this.nodes[nodeID0];
		var node1 = this.nodes[nodeID1];

		var linkID = link.id;
		node0.linkIDs.push(linkID);
		node1.linkIDs.push(linkID);

		node0.neighborIDs.push(nodeID1);
		node1.neighborIDs.push(nodeID0);
	}
}
{
	Map.random = function(size, margin, numberOfNodes)
	{
		var sizeMinusMargins = 
			size.clone().subtract(margin).subtract(margin);

		var nodes = [];
		for (var i = 0; i < numberOfNodes; i++)
		{
			var pos = new Coords().random().multiply
			(
				sizeMinusMargins
			).add
			(
				margin
			);
			var nodeID = "_" + i;
			var node = new MapNode(nodeID, pos);
			nodes.push(node);
		}

		var links = [];
		var nodesNotYetLinked = nodes.slice(1);
		var nodesLinked = [nodes[0]];
		var displacement = new Coords();
		while (nodesNotYetLinked.length > 0)
		{
			var distanceMinSoFar = Number.POSITIVE_INFINITY;
			var nodeNearestSoFar = null;

			for (var i = 0; i < nodesLinked.length; i++)
			{
				var nodeI = nodesLinked[i];
				var nodeIPos = nodeI.pos;

				for (var j = 0; j < nodesNotYetLinked.length; j++)
				{				
					var nodeJ = nodesNotYetLinked[j];

					if (nodeJ != nodeI)
					{
						var nodeJPos = nodeJ.pos;

						displacement.overwriteWith
						(
							nodeJPos
						).subtract
						(
							nodeIPos
						);

						var distance = displacement.magnitude();
						if (distance < distanceMinSoFar)
						{
							distanceMinSoFar = distance;
							nodesNearestSoFar = [nodeI, nodeJ];
						}
					}					
				}
			}

			var nodeToLinkFrom = nodesNearestSoFar[0];
			var nodeToLinkTo = nodesNearestSoFar[1];
			nodesNotYetLinked.splice
			(
				nodesNotYetLinked.indexOf(nodeToLinkTo), 1
			);
			nodesLinked.push(nodeToLinkTo);

			var idsOfNodesToLink = 
			[
				nodeToLinkFrom.id,
				nodeToLinkTo.id
			];
			var linkID = "_" + links.length;
			var link = new MapLink(linkID, idsOfNodesToLink);
			links.push(link);
		}

		var returnValue = new Map(size, nodes, links);
		return returnValue;
	}

	// instance methods

	Map.prototype.drawToDisplay = function(display)
	{
		display.colorFore("Gray");

		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			var nodes = link.nodes(this);
			var node0Pos = nodes[0].pos;
			var node1Pos = nodes[1].pos;
			display.drawLine(node0Pos, node1Pos);
		}

		var nodeSize = new Coords(2, 2);

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			var nodePos = node.pos;
			display.drawRectangle(nodePos, nodeSize);
		}
	}

	Map.prototype.nodeNearestToPos = function(posToCheck)
	{
		var node = this.nodes[0];
		var displacement = node.pos.clone().subtract
		(
			posToCheck
		);

		var nodeNearestSoFar = node;
		var distanceNearestSoFar = displacement.magnitude();

		for (var i = 1; i < this.nodes.length; i++)
		{
			node = this.nodes[i];
			var nodeDistance = displacement.overwriteWith
			(
				node.pos
			).subtract
			(
				posToCheck
			).magnitude();

			if (nodeDistance < distanceNearestSoFar)
			{
				distanceNearestSoFar = nodeDistance;
				nodeNearestSoFar = node;
			}
		}

		return nodeNearestSoFar;
	}
}

function MapLink(id, nodeIDs)
{
	this.id = id;
	this.nodeIDs = nodeIDs;
}
{
	MapLink.prototype.nodes = function(map)
	{
		var returnValues = [];
		for (var i = 0; i < this.nodeIDs.length; i++)
		{
			var nodeID = this.nodeIDs[i];
			var node = map.nodes[nodeID];
			returnValues.push(node);
		}
		return returnValues;
	}
}

function MapNode(id, pos)
{
	this.id = id;
	this.pos = pos;

	this.linkIDs = [];
	this.neighborIDs = [];
}

function Route(nodeIDs)
{
	this.nodeIDs = nodeIDs;
}
{
	Route.prototype.fromNodeIDToNodeID = function(map, fromNodeID, toNodeID)
	{
		var mapNodes = map.nodes;

		var fromNode = mapNodes[fromNodeID];
		var toNode = mapNodes[toNodeID];
		var displacementToGoal = toNode.pos.clone().subtract
		(
			fromNode
		);

		var nodeIDsToConsider = [];
		var nodeIDsConsidered = [];

		nodeIDsToConsider.push(fromNodeID);

		var nodeIDToDistanceLookup = [];
		var nodeIDToPredecessorLookup = [];

		while (nodeIDsToConsider.length > 0)
		{
			var nodeIDToConsider = nodeIDsToConsider[0];

			nodeIDsToConsider.splice(0, 1);
			nodeIDsConsidered.splice(0, 0, nodeIDToConsider);

			var nodeToConsider = mapNodes[nodeIDToConsider];
			var neighborIDs = nodeToConsider.neighborIDs;
			for (var n = 0; n < neighborIDs.length; n++)
			{
				var neighborID = neighborIDs[n];

				if (nodeIDsConsidered.indexOf(neighborID) == -1)
				{
					if (nodeIDsToConsider.indexOf(neighborID) == -1)
					{
						nodeIDToPredecessorLookup[neighborID] = nodeIDToConsider;

						var neighbor = mapNodes[neighborID];

						var neighborDistance = displacementToGoal.overwriteWith
						(
							toNode.pos
						).subtract
						(
							neighbor.pos 
						).magnitude();

						if (neighborDistance == 0)
						{
							this.nodeIDs.length = 0;
							var nodeIDCurrent = neighborID;
							while (nodeIDCurrent != null)
							{
								this.nodeIDs.splice(0, 0, nodeIDCurrent);
								nodeIDCurrent = nodeIDToPredecessorLookup[nodeIDCurrent];
							}

							nodeIDsToConsider.length = 0;
							break;
						}

						nodeIDToDistanceLookup[neighborID] = neighborDistance;
						nodeIDToPredecessorLookup[neighborID] = nodeIDToConsider;

						var i;
						for (i = 0; i < nodeIDsToConsider.length; i++)
						{
							var nodeSortedID = nodeIDsToConsider[i];
							var nodeSortedDistance = nodeIDToDistanceLookup[nodeSortedID];
							if (neighborDistance < nodeSortedDistance)
							{
								break;
							}
						}
						nodeIDsToConsider.splice(i, 0, neighborID);
					}
				}
			}
		}

		return this;
	}

	// drawable

	Route.prototype.drawToDisplayForMapAndAgent = function(display, map, agent)
	{
		var nodePosPrev = agent.pos;
		display.colorFore("Black");

		for (var i = 0; i < this.nodeIDs.length; i++)
		{
			var nodeID = this.nodeIDs[i];
			var node = map.nodes[nodeID];
			var nodePos = node.pos;
			display.drawLine(nodePosPrev, nodePos);

			nodePosPrev = nodePos;
		}
	}
}

function Target(id, pos)
{
	this.id = id;
	this.pos = pos;
}

function World(map, agents, targets)
{
	this.map = map;
	this.agents = agents.addLookups("id");
	this.targets = targets.addLookups("id");
}
{
	World.random = function(map, numberOfAgents, numberOfTargets)
	{
		var mapNodes = map.nodes;

		var agents = [];

		for (var i = 0; i < numberOfAgents; i++)
		{
			var nodeStart = mapNodes[i];
			var nodeEnd = mapNodes[mapNodes.length - 1 - i];
			var agent = new Agent
			(
				"_" + i, 
				nodeStart.pos.clone()
			);
			agents.push(agent);
		}

		var targets = [];

		for (var i = 0; i < numberOfTargets; i++)
		{
			var pos = new Coords().random().multiply(map.size);
			var targetID = IDHelper.idNext();
			var target = new Target(targetID, pos);
			targets.push(target);
		}

		var returnValue = new World
		(
			map, 
			agents,
			targets
		);

		return returnValue;
	}

	// instance methods

	World.prototype.drawToDisplay = function(display)
	{
		display.colorFore("Gray");
		var targetSize = new Coords(3, 3);

		for (var i = 0; i < this.targets.length; i++)
		{
			var target = this.targets[i];
			display.drawRectangle(target.pos, targetSize);
		}

		this.map.drawToDisplay(display);

		var agentRadius = 5;
		for (var i = 0; i < this.agents.length; i++)
		{
			var agent = this.agents[i];
			display.drawCircle(agent.pos, agentRadius);
			
			var route = agent.route;
			route.drawToDisplayForMapAndAgent(display, this.map, agent);
		}
	}

	World.prototype.updateForTimerTick = function()
	{
		for (var i = 0; i < this.agents.length; i++)
		{
			var agent = this.agents[i];
			agent.act(this);
		}
	}
}

// run

main();

</script>
</body>
</html>
This entry was posted in Uncategorized and tagged , , , , , . Bookmark the permalink.

Leave a comment