Simulating a Simple World in JavaScript

The code listing included below uses concepts from previous posts on this blog (namely, the ones where a primitive orrery and an a-star pathfinding algorithm were implemented) to create a small microcosm in which several agents move from station to station, each agent randomly choosing a new destination upon arriving at its current one.

To see the code in action, copy it to an .html file and open that file in a web browser that runs JavaScript.

<html>
<body>
<script type='text/javascript'>
function Terrarium()
{
	this.main = function()
	{
		var sizeStandard = new Coords(16, 16);

		var map0 = new Map
		(
			"Map 0",
			sizeStandard,
			new Array
			(
				"..................~.............",
				"..................~.............",
				"...~~~............~.............",
				".....~...........~~.....~~~~~~~.",
				".....~...........~......~.....~.",
				".....~~~~~.......~......~.....~.",
				".........~~~.....~~.....~.....~.",
				"...........~......~.....~.....~.",
				".........~~~......~.....~~~..~~.",
				".....~~~~~......................",
				".....~..........................",
				".....~..........................",
				".....~..........................",
				".....~..........................",
				".....~..........................",
				".....~.........................."
			)
		);

		var bodyDefnMoverBlue 	= new BodyDefn("Blue Mover", 	Color_Instances.Blue, 	sizeStandard);
		var bodyDefnMoverGreen 	= new BodyDefn("Green Mover", 	Color_Instances.Green, 	sizeStandard);
		var bodyDefnMoverOrange = new BodyDefn("Orange Mover", 	Color_Instances.Orange, sizeStandard);
		var bodyDefnMoverPurple = new BodyDefn("Purple Mover", 	Color_Instances.Purple, sizeStandard);
		var bodyDefnMoverRed 	= new BodyDefn("Red Mover", 	Color_Instances.Red, 	sizeStandard);
		var bodyDefnMoverYellow = new BodyDefn("Yellow Mover", 	Color_Instances.Yellow, sizeStandard);

		var bodyDefnStation = new BodyDefn("Station", Color_Instances.Black, sizeStandard);

		var world0 = new World
		(
			"World0",
			map0,
			new Array
			(
				new Body("Station0", bodyDefnStation, new Coords(0, 0)),
				new Body("Station1", bodyDefnStation, new Coords(13, 2)),
				new Body("Station2", bodyDefnStation, new Coords(2, 14)),
				new Body("Station3", bodyDefnStation, new Coords(28, 6)),
				new Body("Station4", bodyDefnStation, new Coords(19, 12))
			),
			new Array
			(
				new Body("Mover0", bodyDefnMoverBlue, 	new Coords(1, 1)),
				new Body("Mover1", bodyDefnMoverGreen, 	new Coords(2, 2)),
				new Body("Mover2", bodyDefnMoverOrange, new Coords(3, 3)),
				new Body("Mover3", bodyDefnMoverPurple, new Coords(4, 2)),
				new Body("Mover4", bodyDefnMoverRed, 	new Coords(5, 1)),
				new Body("Mover5", bodyDefnMoverYellow, new Coords(6, 2))

			)
		);

		Globals.Instance = new Globals();

		Globals.Instance.initialize
		(
			100, //realWorldMillisecondsPerTick,
			new Coords(640, 480), //viewSizeInPixels, 
			world0
		);
	}
}

// classes

function Activity(actor, defn, target)
{
	this.actor = actor;
	this.defn = defn;
	this.target = target;
}
{
	var prototype = Activity.prototype;

	prototype.initialize = function()
	{
		this.defn.initialize(this);
	}

	prototype.perform = function()
	{
		this.defn.perform(this);
	}
}

function Activity_Variables()
{}

function ActivityDefn_Instances()
{}
{
	ActivityDefn_Instances.DoNothing = new function()
	{
		this.initialize = function() {}

		this.perform = function(activity)
		{
			// do nothing
		}
	}

	ActivityDefn_Instances.MoveBetweenStations = function()
	{}
	{
		var prototype = ActivityDefn_Instances.MoveBetweenStations.prototype;

		prototype.initialize = function(activity)
		{
			var loc = activity.actor.loc;
			var map = loc.venue.map;
			var posInCells = loc.pos.clone().divide(map.cellSizeInPixels).floor();

			activity.vars = new Activity_Variables();		}

		prototype.perform = function(activity)
		{
			var loc = activity.actor.loc;
			var venue = loc.venue;
			var map = venue.map;
			var cellSizeInPixels = map.cellSizeInPixels;

			if (activity.vars.path == null)
			{
				var numberOfStations = venue.stations.length;
				var stationIndexRandom = Math.floor(Math.random() * numberOfStations);
				var stationToMoveTo = venue.stations[stationIndexRandom];
				var targetPosInCells = stationToMoveTo.loc.pos.clone().divide(cellSizeInPixels).floor();
				var posInCells = loc.pos.clone().divide(cellSizeInPixels);

				activity.vars.path = new Path(map, posInCells, targetPosInCells);
				activity.vars.nodeIndex = 0;	
			}

			var path = activity.vars.path;
			var targetPosInCells = path.nodes[activity.vars.nodeIndex].cellPos;

			var targetPosInPixels = targetPosInCells.clone().multiply(cellSizeInPixels);
			var displacementToTarget = targetPosInPixels.clone().subtract(loc.pos);
			var distanceToTarget = displacementToTarget.magnitude();

			var distancePerTick = 4;
			if (distanceToTarget <= distancePerTick)
			{
				loc.pos.overwriteWith(targetPosInPixels);
				activity.vars.nodeIndex++;
				if (activity.vars.nodeIndex >= path.nodes.length)
				{
					activity.vars.path = null;
				}
			}
			else
			{
				var directionToTarget = displacementToTarget.clone().directions().multiplyScalar(distancePerTick);
				loc.pos.add(directionToTarget);	
			}
		}
	}
}

function Body(name, defn, pos)
{
	this.name = name;
	this.defn = defn;
	this.loc = new Location(null, pos);
}
{
	Body.BodyText = "Body";

	var prototype = Body.prototype;

	prototype.activity_Get = function() { return this._activity; }
	prototype.activity_Set = function(value) 
	{ 
		this._activity = value; 
		this._activity.initialize(); 
	}

	prototype.htmlElementBuild = function()
	{
		var pos = this.loc.pos;
		var size = this.defn.size;
		var sizeHalf = this.defn.sizeHalf;

		var returnValue = document.createElement("div");
		returnValue.id = Body.BodyText + this.name;
		returnValue.style.backgroundColor = this.defn.color.systemColor;
		returnValue.style.width = size.x + "px";
		returnValue.style.height = size.y + "px";
		returnValue.style.position = "absolute";
		returnValue.style.left = (pos.x + sizeHalf.x) + "px";
		returnValue.style.top = (pos.y + sizeHalf.y) + "px";

		this.htmlElement = returnValue;

		return this.htmlElement;
	}

	prototype.htmlElementUpdate = function()
	{
		var pos = this.loc.pos;
		var sizeHalf = this.defn.sizeHalf;

		this.htmlElement.style.left = (pos.x + sizeHalf.x) + "px";
		this.htmlElement.style.top = (pos.y + sizeHalf.y) + "px";
	}
}

function BodyDefn(name, color, size)
{
	this.name = name;
	this.color = color;
	this.size = size;
	this.sizeHalf = this.size.clone().divideScalar(2);
}

function Color(name, systemColor)
{
	this.name = name;
	this.systemColor = systemColor;
}

function Color_Instances()
{}
{
	Color_Instances.Black 	= new Color("Black", 	"#000000");
	Color_Instances.Blue 	= new Color("Blue", 	"#0000ff");
	Color_Instances.Gray 	= new Color("Gray", 	"#808080");
	Color_Instances.Green 	= new Color("Green", 	"#00ff00");
	Color_Instances.Orange 	= new Color("Orange", 	"#ff8800");
	Color_Instances.Purple 	= new Color("Purple", 	"#ff00ff");
	Color_Instances.Red 	= new Color("Red", 	"#ff0000");
	Color_Instances.White 	= new Color("White", 	"#ffffff");
	Color_Instances.Yellow 	= new Color("Yellow", 	"#ffff00");
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	var prototype = Coords.prototype;

	prototype.absolute = function()
	{
		this.x = Math.abs(this.x);
		this.y = Math.abs(this.y);

		return this;
	}

	prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;

		return this;
	}

	prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;

		return this;
	}

	prototype.clone = function()
	{
		var returnValue = new Coords(this.x, this.y);

		return returnValue;
	}

	prototype.directions = function()
	{
		if (this.x > 0)
		{
			this.x = 1;
		}
		else if (this.x < 0)
		{
			this.x = -1;
		}

		if (this.y > 0)
		{
			this.y = 1;
		}
		else if (this.y < 0)
		{
			this.y = -1;
		}

		return this;
	}

	prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;

		return this;
	}

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

		return this;
	}

	prototype.equals = function(other)
	{
		return (this.x == other.x && this.y == other.y);
	}

	prototype.floor = function()
	{
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);

		return this;
	}

	prototype.magnitude = function()
	{
		var returnValue = Math.sqrt(this.x * this.x + this.y * this.y);

		return returnValue;
	}

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

		return this;
	}

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

		return this;
	}

	prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

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

		return this;
	}

	prototype.round = function()
	{
		this.x = Math.round(this.x);
		this.y = Math.round(this.y);

		return this;
	}

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

		return this;
	}

	prototype.sumOfXAndY = function()
	{
		return this.x + this.y;
	}

	prototype.toString = function()
	{
		return "<Coords x='" + this.x + "' y='" + this.y + "' />"
	}
}

function Globals()
{
	this.initialize = function
	(
		realWorldMillisecondsPerTick, 
		viewSizeInPixels, 
		world
	)	
	{
		this.realWorldMillisecondsPerTick = realWorldMillisecondsPerTick;
		this.viewSizeInPixels = viewSizeInPixels;
		this.viewSizeInPixelsHalf = this.viewSizeInPixels.clone().divideScalar(2);

		this.world = world;

		this.world.initialize();

		this.world.htmlElementBuild();

		document.body.appendChild(world.htmlElement);

		setInterval("Globals.Instance.processTick()", this.realWorldMillisecondsPerTick);
	}

	this.processTick = function()
	{
		this.world.update();
	}
}

function Image(name, filePath)
{
	this.name = name;
	this.filePath = filePath;
}

function Location(venue, pos)
{
	this.venue = venue;
	this.pos = pos;
}

function Map(name, cellSizeInPixels, cellsAsStrings)
{
	this.cellsAsStrings = cellsAsStrings;	
	this.cellSizeInPixels = cellSizeInPixels;
	this.sizeInCells = new Coords
	(
		this.cellsAsStrings[0].length, 
		this.cellsAsStrings.length
	);
}
{
	var prototype = Map.prototype;

	prototype.cellAtPos = function(cellPos)
	{
		var codeChar = this.cellsAsStrings[cellPos.y][cellPos.x];

		return new MapCell
		(
			MapTerrainInstances._Lookup[codeChar]
		);
	}

	prototype.htmlElementBuild = function()
	{
		var returnValue = document.createElement("table");
		returnValue.style.backgroundColor = "#888888";
		returnValue.cellPadding = 0;
		returnValue.border = 0;
		returnValue.cellSpacing = 0;

		for (var y = 0; y < this.cellsAsStrings.length; y++)
		{
			var trForRow = document.createElement("tr");

			var cellRowAsString = this.cellsAsStrings[y];

			for (var x = 0; x < cellRowAsString.length; x++)
			{
				var cellAsChar = cellRowAsString[x];

				tdForCell = document.createElement("td");

				tdForCell.style.backgroundColor = MapTerrainInstances._Lookup[cellAsChar].color;
				tdForCell.style.width = this.cellSizeInPixels.x;
				tdForCell.style.height = this.cellSizeInPixels.y; 

				trForRow.appendChild(tdForCell);
			}

			returnValue.appendChild(trForRow);
		}

		this.htmlElement = returnValue;

		return returnValue;
	}
}

function MapCell(terrain)
{
	this.terrain = terrain;
}

function MapTerrain(name, codeChar, color, costToTraverse)
{
	this.name = name;
	this.codeChar = codeChar;
	this.color = color;
	this.costToTraverse = costToTraverse;
}

function MapTerrainInstances()
{}
{
	MapTerrainInstances.Plain = new MapTerrain("plain", ".", "#00aa00", 1);
	MapTerrainInstances.Water = new MapTerrain("water", "~", "#0000aa", 1000000);

	MapTerrainInstances._All = new Array
	(
		MapTerrainInstances.Plain,
		MapTerrainInstances.Water
	);

	MapTerrainInstances.BuildLookup = function()
	{
		var returnValue = new Array();

		var numberOfTerrains = MapTerrainInstances._All.length;

		for (var i = 0; i < numberOfTerrains; i++)
		{
			var terrain = MapTerrainInstances._All[i];

			returnValue[terrain.codeChar] = terrain;
		}

		return returnValue;
	}

	MapTerrainInstances._Lookup = MapTerrainInstances.BuildLookup();
}

function Path(map, startPos, goalPos)
{
	this.map = map;
	this.startPos = startPos;
	this.goalPos = goalPos;

	var openList = new Array();
	var openLookup = new Array();
	var closedLookup = new Array();

	var startNode = new PathNode
	(
		startPos,
		0,
		goalPos.clone().subtract(startPos).absolute().sumOfXAndY(),
		null
	);

	openList.push(startNode);
	var startIndex = "_" + (startNode.cellPos.y * map.sizeInCells.x + startNode.cellPos.x);
	openLookup[startIndex] = startNode;

	while (openList.length > 0)
	{
		var current = openList[0];

		if (current.cellPos.equals(goalPos) == true)
		{	
			this.nodes = new Array();

			while (current != null)
			{
				this.nodes.splice(0, 0, current);
				current = current.prev;
			}
			break;
		}

		openList.splice(0, 1);
		var currentIndex = "_" + (current.cellPos.y * map.sizeInCells.x + current.cellPos.x);
		delete openLookup[currentIndex];

		closedLookup[currentIndex] = current;

		var neighbors = this.getNeighborsForNode(map, current, goalPos);

		for (var n = 0; n < neighbors.length; n++)
		{
			var neighbor = neighbors[n];
			var neighborPos = neighbor.cellPos;

			var neighborIndex = "_" + (neighborPos.y * map.sizeInCells.x + neighborPos.x);

			if (closedLookup[neighborIndex] == null && openLookup[neighborIndex] == null)
			{
				var i;
				for (i = 0; i < openList.length; i++)
				{
					var nodeFromOpenList = openList[i];
					if (neighbor.costFromStart < nodeFromOpenList.costFromStart)
					{
						break;
					}
				}

				openList.splice(i, 0, neighbor);
				openLookup[neighborIndex] = neighbor;
			}
		}
	}	
}
{
	var prototype = Path.prototype;

	prototype.convertToHTMLElement = function()
	{
		var returnValue = document.createElement("div");
		returnValue.cellPadding = 0;
		returnValue.border = 0;
		returnValue.cellSpacing = 0;

		var cellSizeInPixels = this.map.cellSizeInPixels;

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			var nodeCellPos = node.cellPos;

			var divForNode = document.createElement("div");
			divForNode.style.position = "absolute";
			divForNode.style.width = cellSizeInPixels.x + "px";
			divForNode.style.height = cellSizeInPixels.y + "px";
			divForNode.style.left = (nodeCellPos.x * cellSizeInPixels.x) + "px";
			divForNode.style.top = (nodeCellPos.y * cellSizeInPixels.y) + "px";
			divForNode.style.backgroundColor = "#ff0000";	

			returnValue.appendChild(divForNode);
		}

		return returnValue;
	}

	prototype.getNeighborsForNode = function(map, node, goalPos)
	{
		var returnValues = new Array();
		var cellPos = node.cellPos;

		var neighborPositions = new Array();

		if (cellPos.x > 0)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x - 1, cellPos.y)
			);

		}
		if (cellPos.x < map.sizeInCells.x - 1)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x + 1, cellPos.y)
			);
		}		
		if (cellPos.y > 0)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x, cellPos.y - 1)
			);
		}
		if (cellPos.y < map.sizeInCells.y - 1)
		{
			neighborPositions.push
			(
				new Coords(cellPos.x, cellPos.y + 1)
			);
		}		

		var tempPos = new Coords(0, 0);

		for (var i = 0; i < neighborPositions.length; i++)
		{
			var neighborPos = neighborPositions[i];

			var costToTraverse = map.cellAtPos(neighborPos).terrain.costToTraverse;

			var neighborNode = new PathNode
			(
				neighborPos,
				node.costFromStart + costToTraverse,
				costToTraverse + goalPos.clone().subtract(cellPos).absolute().sumOfXAndY(),
				node
			);

			returnValues.push(neighborNode);
		}

		return returnValues;
	}
}

function PathNode(cellPos, costFromStart, costToGoalEstimated, prev)
{
	this.cellPos = cellPos;
	this.costFromStart = costFromStart;
	this.costToGoalEstimated = costToGoalEstimated;
	this.prev = prev;
}

function World(name, map, stations, movers)
{
	this.name = name;
	this.map = map;
	this.stations = stations;
	this.movers = movers;

	this.bodies = new Array();

	for (var i = 0; i < this.stations.length; i++)
	{
		this.bodies.push(this.stations[i]);
	}

	for (var i = 0; i < this.movers.length; i++)
	{
		this.bodies.push(this.movers[i]);
	}
}
{
	var prototype = World.prototype;

	prototype.htmlElementBuild = function()
	{
		var returnValue = document.createElement("div");

		returnValue.appendChild(this.map.htmlElementBuild());

		var numberOfBodies = this.bodies.length;
		for (var i = 0; i < numberOfBodies; i++)
		{
			var body = this.bodies[i];

			returnValue.appendChild(body.htmlElementBuild());
		}

		this.htmlElement = returnValue;

		return returnValue;
	}

	prototype.htmlElementUpdate = function()
	{
		var numberOfMovers = this.movers.length;
		for (var i = 0; i < numberOfMovers; i++)
		{
			var mover = this.movers[i];

			mover.activity_Get().perform();
		}
	}

	prototype.initialize = function()
	{
		var numberOfBodies = this.bodies.length;
		for (var i = 0; i < numberOfBodies; i++)
		{
			var body = this.bodies[i];

			body.loc.venue = this;
			body.loc.pos.multiply(this.map.cellSizeInPixels);

			body.activity_Set
			(
				new Activity
				(
					body,
					new ActivityDefn_Instances.MoveBetweenStations
					(
						new Coords(10, 10)
					),
					null
				)
			);
		}
	}

	prototype.update = function()
	{
		var numberOfBodies = this.bodies.length;
		for (var i = 0; i < numberOfBodies; i++)
		{
			var body = this.bodies[i];

			body.htmlElementUpdate();
		}
		this.htmlElementUpdate();
	}
}

new Terrarium().main();

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s