A Brick-Breaking Game in JavaScript

Below is a simple brick-breaking game implemented in JavaScript. Its gameplay is similar to the classic games Breakout and Arkanoid. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

BrickBreakingGame.png


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var displaySize = new Coords(100, 200);

	var display = new Display(displaySize);

	var world = World.random(displaySize);

	Globals.Instance.initialize
	(
		10, // ticksPerSecond
		display,
		world
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Activity(perform)
{
	this.perform = perform;
}
{
	Activity.Instances = new Activity_Instances()
	
	function Activity_Instances()
	{
		this.DoNothing = new Activity(function perform() {});
		this.UserInputAccept = new Activity
		(
			function perform(world, actor)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var inputsActive = inputHelper.keysPressed;

				for (var i = 0; i < inputsActive.length; i++)
				{
					var inputActive = inputsActive[i];
					if (inputActive == "ArrowLeft")
					{
						actor.vel.x -= actor.accelPerTick;
					}
					else if (inputActive == "ArrowRight")
					{
						actor.vel.x += actor.accelPerTick;
					}
				}
			}
		);
	}
}

function Actor(pos, activity)
{
	this.pos = pos;
	this.activity = activity;

	this.color = "Gray";
	this.radius = 8;

	this.vel = new Coords(0, 0);

	this.accelPerTick = .005;
	this.speedMax = .25;

	this.projectileRadius = this.radius / 4;
	this.projectileSpeed = .3;

	// Helper variables.

	this.coordsTemp = new Coords();
	this.vertices = 
	[
		new Coords(), new Coords(), new Coords()
	];
}
{
	Actor.prototype.updateForTimerTick = function(world)
	{
		this.activity.perform(world, this);

		var speed = this.vel.magnitude();
		if (speed >= this.speedMax)
		{
			this.vel.normalize().multiplyScalar(this.speedMax);
		}

		this.pos.add(this.vel);
		this.pos.trimToRangeMinMax
		(
			world.actorPosMin,
			world.actorPosMax
		);

		this.vel.multiplyScalar(.98); // friction
	}

	// drawable

	Actor.prototype.drawToDisplay = function(display)
	{
		display.drawCircle
		(
			this.pos, this.radius, "Gray"
		);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}

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

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

	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.dotProduct = function(other)
	{
		return this.x * other.x + this.y * other.y;
	}

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

	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.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

	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.right = function()
	{
		var temp = this.x;
		this.x = 0 - this.y;
		this.y = temp;
		return this;
	}

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

	Coords.prototype.trimToRangeMinMax = function(min, max)
	{
		if (this.x < min.x)
		{
			this.x = min.x;
		}
		else if (this.x > max.x)
		{
			this.x = max.x;
		}

		while (this.y < min.y)
		{
			this.y = min.y;
		}
		while (this.y > max.y)
		{
			this.y = max.y;
		}

		return this;
	}
}

function CollisionHelper()
{
	this.displacement = new Coords();
	this.edgeForward = new Coords();
	this.edgeRight = new Coords();
}
{
	CollisionHelper.Instance = new CollisionHelper();

	CollisionHelper.prototype.doCirclesCollide = function
	(
		circle0Center, circle0Radius, circle1Center, circle1Radius
	)
	{
		var distanceBetweenCenters = this.displacement.overwriteWith
		(
			circle1Center
		).subtract
		(
			circle0Center
		).magnitude();

		var sumOfRadii = circle0Radius + circle1Radius;

		var returnValue = (distanceBetweenCenters < sumOfRadii);

		return returnValue;
	}
}

function Display(size)
{
	this.size = size;

	this.colorBack = "White";
	this.colorFore = "Gray";
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		this.graphics = canvas.getContext("2d");

		document.body.appendChild(canvas);
	}

	// drawing

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

		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);
	}

	Display.prototype.drawCircle = function(center, radius, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;

		this.graphics.beginPath();
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);
		this.graphics.stroke();
	}

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

	Display.prototype.drawText = function(text, height, pos, color)
	{
		this.graphics.strokeStyle = this.colorBack;
		this.graphics.strokeText(text, pos.x, pos.y + height);

		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y + height);
	}
}

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

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

		this.world = world;

		this.inputHelper = new InputHelper();
		
		var millisecondsPerTimerTick = Math.floor(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);

		this.inputHelper.initialize();
	}

	// events

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

function InputHelper()
{
	this.keysPressed = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	InputHelper.prototype.removeKey = function(key)
	{
		if (this.keysPressed[key] != null)
		{
			this.keysPressed.splice(this.keysPressed.indexOf(key), 1);
			delete this.keysPressed[key];
		}
	}

	// events 

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var key = event.key;
		if (this.keysPressed[key] == null)
		{
			this.keysPressed.push(key);
			this.keysPressed[key] = key;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.removeKey(event.key);
	}

}

function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2;

	Polar.prototype.toCoords = function(coords)
	{
		var azimuthInRadians = this.azimuthInTurns * Polar.RadiansPerTurn; 
		coords.x = Math.cos(azimuthInRadians) * this.radius;
		coords.y = Math.sin(azimuthInRadians) * this.radius;
		return coords;
	}

	Polar.prototype.trimAzimuthToRangeMinMax = function(min, max)
	{
		if (this.azimuthInTurns < min)
		{
			this.azimuthInTurns = min;
		}
		else if (this.azimuthInTurns > max)
		{
			this.azimuthInTurns = max;
		}
		return this;
	}
}

function Obstacle(radius, pos)
{
	this.radius = radius;
	this.pos = pos;
}
{
	// drawable

	Obstacle.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.radius, "Gray");
	}
}

function Projectile(pos, vel)
{
	this.pos = pos;
	this.vel = vel;

	this.radius = 2;
	this.color = "Gray";
}
{
	Projectile.prototype.updateForTimerTick = function(world)
	{
		this.pos.add(this.vel);

		if (this.pos.x < 0 || this.pos.x > world.size.x)
		{
			this.vel.x *= -1;
		}

		if (this.pos.y < 0)
		{
			this.vel.y *= -1;
		} 
		else if (this.pos.y > world.size.y)
		{
			world.projectiles.length = 0;
		}

		this.updateForTimerTick_Collisions(world);
	}

	Projectile.prototype.updateForTimerTick_Collisions = function(world)
	{
		var collisionHelper = CollisionHelper.Instance;

		var obstacles = world.obstacles;

		for (var i = 0; i < obstacles.length; i++)
		{
			var obstacle = obstacles[i];
			var doProjectileAndObstacleCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radius,
				obstacle.pos, obstacle.radius
			);

			if (doProjectileAndObstacleCollide == true)
			{
				this.collideWithOther(obstacle);
				obstacles.remove(obstacle);
				i--;
				break;
			}
		}
	
		var actors = world.actors;

		for (var i = 0; i < actors.length; i++)
		{
			var actor = actors[i];
			var doProjectileAndActorCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radius,
				actor.pos, actor.radius
			);
			if (doProjectileAndActorCollide == true)
			{
				this.collideWithOther(actor);
			}
		}
	}

	// drawable

	Projectile.prototype.drawToDisplay = function(display)
	{
		display.drawLine
		(
			this.pos, 
			this.pos.clone().subtract(this.vel), 
			this.color
		);

		display.drawCircle
		(
			this.pos, this.radius, this.color
		);
	}

	// collidable

	Projectile.prototype.collideWithOther = function(other)
	{
		var posAfterCollision = new Coords();
		var velAfterCollision = new Coords();
		var displacementBetweenCenters = new Coords();
		var velocityRelative = new Coords();
  
		var sumOfBodyRadii = this.radius + other.radius; 
 
		displacementBetweenCenters.overwriteWith
		(
			other.pos
		).subtract
		(
			this.pos
		);
 
		var distanceBetweenCenters = displacementBetweenCenters.magnitude();
 
		var normalAtCollision = displacementBetweenCenters.divideScalar
		(
			distanceBetweenCenters
		);
 
		var velocityAlongNormal = normalAtCollision.multiplyScalar
		(
			this.vel.dotProduct
			(
				normalAtCollision
			)
		);

		this.vel.add
		(
			velocityAlongNormal.multiplyScalar(-2)
		)
	}
}

function World(size, actor, obstacles, projectiles)
{
	this.size = size;
	this.actors = [ actor ];
	this.obstacles = obstacles;
	this.projectiles = projectiles;

	this.actorPosMin = new Coords(actor.radius, actor.pos.y);
	this.actorPosMax = new Coords(this.size.x - actor.radius, actor.pos.y);
}
{
	World.random = function(size)
	{
		var obstacleGridSizeInCells = new Coords(8, 8);
		var spaceBetweenObstacles = new Coords(1, 1).multiplyScalar
		(
			size.x / (obstacleGridSizeInCells.x + 1)
		);

		var actorPos = new Coords
		(
			size.x / 2,
			size.y - spaceBetweenObstacles.y
		);

		var actor = new Actor
		(
			actorPos,
			Activity.Instances.UserInputAccept
		);

		var obstacleRadius = Math.floor(spaceBetweenObstacles.x / 2);

		var obstacles = [];
		for (var y = 1; y <= obstacleGridSizeInCells.y; y++)
		for (var x = 1; x <= obstacleGridSizeInCells.x; x++)
		{
			var pos = new Coords(x, y).multiply(spaceBetweenObstacles);

			var obstacle = new Obstacle
			(
				obstacleRadius, pos
			);

			obstacles.push(obstacle);
		}

		var projectile = new Projectile
		(
			actor.pos.clone().subtract
			(
				new Coords(0, actor.radius + actor.projectileRadius)
			),
			new Coords(1, -1).normalize().multiplyScalar
			(
				actor.projectileSpeed
			)
		);
		
		var projectiles = [ projectile ];

		var returnValue = new World
		(
			size,
			actor,
			obstacles,
			projectiles
		);

		return returnValue;
	}

	// instance methods

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

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.updateForTimerTick(this);
		}
	}

	// drawable

	World.prototype.drawToDisplay = function(display)
	{
		display.clear();

		for (var i = 0; i < this.obstacles.length; i++)
		{
			var obstacle = this.obstacles[i];
			obstacle.drawToDisplay(display);
		}

		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.drawToDisplay(display);
		}

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.drawToDisplay(display);
		}
	}
}

// run

main();

</script>
</body>
</html>

Advertisements
Posted in Uncategorized | Tagged , , | Leave a comment

An Asteroids Clone in JavaScript

Below is a simple clone of the classic video game Asteroids implemented in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

Asteroids.png


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var displaySize = new Coords(200, 200);

	var display = new Display(displaySize);

	var world = World.random(displaySize);

	Globals.Instance.initialize
	(
		10, // ticksPerSecond
		display,
		world
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Activity(perform)
{
	this.perform = perform;
}
{
	Activity.Instances = new Activity_Instances()
	
	function Activity_Instances()
	{
		this.DoNothing = new Activity(function perform() {});
		this.UserInputAccept = new Activity
		(
			function perform(world, actor)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var inputsActive = inputHelper.keysPressed;

				for (var i = 0; i < inputsActive.length; i++)
				{
					var inputActive = inputsActive[i];
					if (inputActive == "ArrowLeft")
					{
						actor.forward.subtract
						(
							actor.right.clone().multiplyScalar
							(
								actor.turnsPerTick
							)
						).normalize();
						actor.right.overwriteWith(actor.forward).right();
					}
					else if (inputActive == "ArrowRight")
					{
						actor.forward.add
						(
							actor.right.clone().multiplyScalar
							(
								actor.turnsPerTick
							)
						).normalize();
						actor.right.overwriteWith(actor.forward).right();
					}
					else if (inputActive == "ArrowUp")
					{
						actor.vel.add
						(
							actor.forward.clone().multiplyScalar
							(
								actor.accelPerTick
							)
						);
					}
					else if (inputActive == "Enter")
					{
						if (world.projectiles.length > 0)
						{
							return;
						}

						var projectilePos = actor.forward.clone().multiplyScalar
						(
							actor.lengthHalf
						).add
						(
							actor.pos
						);

						var projectileVel = actor.forward.clone().multiplyScalar
						(
							actor.projectileSpeed
						);

						var projectile = new Projectile
						(
							projectilePos, projectileVel
						);
						world.projectiles.push(projectile);

						inputHelper.removeKey(inputActive)
					}
				}
			}
		);
	}
}

function Actor(pos, activity)
{
	this.pos = pos;
	this.activity = activity;

	this.color = "Gray";
	this.widthHalf = 3;
	this.lengthHalf = 4;

	this.forward = new Coords(1, 0);
	this.right = this.forward.clone().right();

	this.vel = new Coords(0, 0);

	this.accelPerTick = .0025;
	this.turnsPerTick = .02;
	this.speedMax = .25;
	this.projectileSpeed = 1; 

	// Helper variables.

	this.coordsTemp = new Coords();
	this.vertices = 
	[
		new Coords(), new Coords(), new Coords()
	];
}
{
	Actor.prototype.updateForTimerTick = function(world)
	{
		this.activity.perform(world, this);

		var speed = this.vel.magnitude();
		if (speed >= this.speedMax)
		{
			this.vel.normalize().multiplyScalar(this.speedMax);
		}

		this.pos.add(this.vel);
		this.pos.wrapToRangeMax(world.size);

		var collisionHelper = CollisionHelper.Instance;

		var obstacles = world.obstacles;
		for (var i = 0; i < obstacles.length; i++)
		{
			var obstacle = obstacles[i];
			var doActorAndObstacleCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.widthHalf, // hack
				obstacle.pos, obstacle.radius
			);
			if (doActorAndObstacleCollide == true)
			{
				world.actors.remove(this);
			}
		}
	}

	// drawable

	Actor.prototype.drawToDisplay = function(display)
	{
		this.vertices[0].overwriteWith
		(
			this.forward
		).multiplyScalar
		(
			this.lengthHalf
		).add
		(
			this.pos
		);

		var back = this.coordsTemp.overwriteWith
		(
			this.forward	
		).multiplyScalar
		(
			0 - this.lengthHalf
		).add
		(
			this.pos
		);
		
		this.vertices[1].overwriteWith
		(
			this.right
		).multiplyScalar
		(
			this.widthHalf
		).add
		(
			back
		);

		this.vertices[2].overwriteWith
		(
			this.right
		).multiplyScalar
		(
			0 - this.widthHalf
		).add
		(
			back
		);

		display.drawPolygon(this.vertices, this.color);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}

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

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

	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.dotProduct = function(other)
	{
		return this.x * other.x + this.y * other.y;
	}

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

	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.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

	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.right = function()
	{
		var temp = this.x;
		this.x = 0 - this.y;
		this.y = temp;
		return this;
	}

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

	Coords.prototype.wrapToRangeMax = function(max)
	{
		while (this.x < 0)
		{
			this.x += max.x;
		}
		while (this.x >= max.x)
		{
			this.x -= max.x;
		}

		while (this.y < 0)
		{
			this.y += max.y;
		}
		while (this.y >= max.y)
		{
			this.y -= max.y;
		}

		return this;
	}
}

function CollisionHelper()
{
	this.displacement = new Coords();
	this.edgeForward = new Coords();
	this.edgeRight = new Coords();
}
{
	CollisionHelper.Instance = new CollisionHelper();

	CollisionHelper.prototype.doCirclesCollide = function
	(
		circle0Center, circle0Radius, circle1Center, circle1Radius
	)
	{
		var distanceBetweenCenters = this.displacement.overwriteWith
		(
			circle1Center
		).subtract
		(
			circle0Center
		).magnitude();

		var sumOfRadii = circle0Radius + circle1Radius;

		var returnValue = (distanceBetweenCenters < sumOfRadii);

		return returnValue;
	}
}

function Display(size)
{
	this.size = size;

	this.colorBack = "White";
	this.colorFore = "Gray";
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		this.graphics = canvas.getContext("2d");

		document.body.appendChild(canvas);
	}

	// drawing

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

		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);
	}

	Display.prototype.drawCircle = function(center, radius, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;

		this.graphics.beginPath();
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);
		this.graphics.stroke();
	}

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

	Display.prototype.drawPolygon = function(vertices, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;
		this.graphics.beginPath();
		var vertex = vertices[0];
		this.graphics.moveTo(vertex.x, vertex.y);
		for (var i = 1; i < vertices.length; i++)
		{
			vertex = vertices[i];
			this.graphics.lineTo(vertex.x, vertex.y);
		}
		this.graphics.closePath();
		this.graphics.stroke();
	}


	Display.prototype.drawRectangle = function(pos, size, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;
		this.graphics.strokeRect
		(
			pos.x, pos.y, size.x, size.y
		);
	}

	Display.prototype.drawText = function(text, height, pos, color)
	{
		this.graphics.strokeStyle = this.colorBack;
		this.graphics.strokeText(text, pos.x, pos.y + height);

		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y + height);
	}
}

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

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

		this.world = world;

		this.inputHelper = new InputHelper();
		
		var millisecondsPerTimerTick = Math.floor(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);

		this.inputHelper.initialize();
	}

	// events

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

function InputHelper()
{
	this.keysPressed = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	InputHelper.prototype.removeKey = function(key)
	{
		if (this.keysPressed[key] != null)
		{
			this.keysPressed.splice(this.keysPressed.indexOf(key), 1);
			delete this.keysPressed[key];
		}
	}

	// events 

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var key = event.key;
		if (this.keysPressed[key] == null)
		{
			this.keysPressed.push(key);
			this.keysPressed[key] = key;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.removeKey(event.key);
	}

}

function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2;

	Polar.prototype.toCoords = function(coords)
	{
		var azimuthInRadians = this.azimuthInTurns * Polar.RadiansPerTurn; 
		coords.x = Math.cos(azimuthInRadians) * this.radius;
		coords.y = Math.sin(azimuthInRadians) * this.radius;
		return coords;
	}

	Polar.prototype.trimAzimuthToRangeMinMax = function(min, max)
	{
		if (this.azimuthInTurns < min)
		{
			this.azimuthInTurns = min;
		}
		else if (this.azimuthInTurns > max)
		{
			this.azimuthInTurns = max;
		}
		return this;
	}
}

function Obstacle(radius, pos, vel)
{
	this.radius = radius;
	this.pos = pos;
	this.vel = vel;
}
{
	Obstacle.prototype.updateForTimerTick = function(world)
	{
		this.pos.add(this.vel).wrapToRangeMax(world.size);

		var collisionHelper = CollisionHelper.Instance;
		var obstacles = world.obstacles;
		for (var i = 0; i < obstacles.length; i++)
		{
			var other = obstacles[i];
			if (other != this)
			{
				var doThisAndOtherCollide = collisionHelper.doCirclesCollide
				(
					this.pos, this.radius,
					other.pos, other.radius
				);

				if (doThisAndOtherCollide == true)
				{
					this.collideWithOther(other);
				}
			}
		}
	}

	// drawable

	Obstacle.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.radius, "Gray");
	}

	// collidable

	Obstacle.prototype.collideWithOther = function(other)
	{
		var bodyPositionsAfterCollision = [new Coords(), new Coords()];
		var bodyVelsAfterCollision = [new Coords(), new Coords()];
		var displacement = new Coords();
		var velocityRelative = new Coords();
 
		var bodies = [ this, other ];
		var body0 = bodies[0];
		var body1 = bodies[1];
 
		var sumOfBodyRadii = 
			body0.radius + body1.radius; 
 
		velocityRelative.overwriteWith
		(
			body0.vel
		).subtract
		(
			body1.vel
		);
  
		displacement.overwriteWith
		(
			body0.pos
		).subtract
		(
			body1.pos
		);
 
		var distanceBetweenBodyCenters = displacement.magnitude();
		var overlap = sumOfBodyRadii - distanceBetweenBodyCenters;
		var overlapHalf = overlap / 2;
 
		var normalAtCollision = displacement.divideScalar
		(
			distanceBetweenBodyCenters
		);
 
		var velocityAlongNormal = normalAtCollision.multiplyScalar
		(
			velocityRelative.dotProduct
			(
				normalAtCollision
			)
		);
 
		velocityRelative.subtract
		(
			velocityAlongNormal
		).multiplyScalar
		(
			-1
		);
 
		for (var i = 0; i < bodies.length; i++)
		{
			var bodyThis = bodies[i];
			var bodyOther = bodies[1 - i];
		 
			var bodyPosAfterCollision = bodyPositionsAfterCollision[i];
			var bodyVelAfterCollision = bodyVelsAfterCollision[i];
 
			var multiplier = (i == 0 ? -1 : 1);
 
			bodyPosAfterCollision.overwriteWith
			(
				normalAtCollision
			).multiplyScalar
			(
				multiplier * overlapHalf
			).add
			(
				bodyThis.pos
			);
  
			bodyVelAfterCollision.overwriteWith
			(
				velocityRelative
			).multiplyScalar
			(
				multiplier
			).add
			(
				bodyOther.vel
			);
		}
 
		for (var i = 0; i < bodies.length; i++)
		{
			var bodyThis = bodies[i];
			var bodyPosAfterCollision = bodyPositionsAfterCollision[i];
			var bodyVelAfterCollision = bodyVelsAfterCollision[i];
 
			bodyThis.pos.overwriteWith
			(
				bodyPosAfterCollision
			);
			bodyThis.vel.overwriteWith
			(
				bodyVelAfterCollision
			);
		}
	}

}

function Projectile(pos, vel)
{
	this.pos = pos;
	this.vel = vel;

	this.radiusInFlight = 2;
	this.colorInFlight = "Gray";

	this.ticksSinceSpawned = 0;
	this.ticksToLive = 100;

	this.ticksSinceExplosion = null;
	this.ticksToExplode = 30;
	this.radiusExplodingMax = 20;
	this.colorExploding = "Gray";
}
{
	Projectile.prototype.drawToDisplay = function(display)
	{
		if (this.ticksSinceExplosion == null)
		{
			display.drawCircle
			(
				this.pos, this.radiusInFlight, this.colorInFlight
			);
			display.drawLine
			(
				this.pos, 
				this.pos.clone().subtract(this.vel), 
				this.colorInFlight
			);
		}
		else
		{
			var radiusCurrent = 
				this.radiusExplodingMax 
				* this.ticksSinceExplosion 
				/ this.ticksToExplode;
			display.drawCircle(this.pos, radiusCurrent, this.colorExploding);
		}
	}

	Projectile.prototype.updateForTimerTick = function(world)
	{
		if (this.ticksSinceSpawned >= this.ticksToLive)
		{
			world.projectiles.remove(this);
		}
		else if (this.ticksSinceExplosion == null)
		{
			this.pos.add(this.vel).wrapToRangeMax(world.size);
			
			this.updateForTimerTick_Obstacles(world);
		}
		else if (this.ticksSinceExplosion < this.ticksToExplode)
		{
			this.ticksSinceExplosion++;
		}
		else
		{
			// todo
		}

		this.ticksSinceSpawned++;
	}

	Projectile.prototype.updateForTimerTick_Obstacles = function(world)
	{
		var collisionHelper = CollisionHelper.Instance;

		var obstacles = world.obstacles;

		for (var i = 0; i < obstacles.length; i++)
		{
			var obstacle = obstacles[i];
			var doProjectileAndObstacleCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radiusInFlight,
				obstacle.pos, obstacle.radius
			);

			if (doProjectileAndObstacleCollide == true)
			{
				world.projectiles.remove(this);
				obstacles.remove(obstacle);
				i--;

				this.updateForTimerTick_Obstacles_Children
				(
					world, obstacle
				);

				break;
			}
		}
	}

	Projectile.prototype.updateForTimerTick_Obstacles_Children = function(world, obstacle)
	{
		var obstacleChildRadius = obstacle.radius / 2;
		if (obstacleChildRadius >= 2)
		{
			for (var c = 0; c < 2; c++)
			{
				var lateral = obstacle.vel.clone().right().normalize().multiplyScalar
				(
				 	(c == 0 ? -1 : 1)
				)

				var displacement = lateral.clone().multiplyScalar
				(
					obstacleChildRadius
				);
				var accel = lateral.clone().multiplyScalar(.1);

				var obstacleChildVel = obstacle.vel.clone().add(accel);

				var obstacleChild = new Obstacle
				(
					obstacleChildRadius,
					obstacle.pos.clone().add(displacement),
					obstacleChildVel
				);
				world.obstacles.push(obstacleChild);
			}
		}
	}
}

function World(size, actor, obstacles)
{
	this.size = size;
	this.actors = [ actor ];
	this.obstacles = obstacles;

	this.projectiles = [];
}
{
	World.random = function(size)
	{
		var actorPos = size.clone().multiplyScalar(.5);

		var actor = new Actor
		(
			actorPos,
			Activity.Instances.UserInputAccept
		);

		var numberOfObstacles = 2;
		var obstacleRadius = 16;

		var obstacles = [];
		var obstacleSpeedMax = .2;
		for (var i = 0; i < numberOfObstacles; i++)
		{
			var pos = new Coords().random().multiply(size);
			var vel = new Polar
			(
				Math.random(), // azimuthInTurns
				Math.random() * obstacleSpeedMax // radius
			).toCoords(new Coords());

			var obstacle = new Obstacle
			(
				obstacleRadius, pos, vel
			);

			obstacles.push(obstacle);
		}

		var returnValue = new World
		(
			size,
			actor,
			obstacles
		);

		return returnValue;
	}

	// instance methods

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

		for (var i = 0; i < this.obstacles.length; i++)
		{
			var obstacle = this.obstacles[i];
			obstacle.updateForTimerTick(this);
		}

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.updateForTimerTick(this);
		}
	}

	// drawable

	World.prototype.drawToDisplay = function(display)
	{
		display.clear();

		for (var i = 0; i < this.obstacles.length; i++)
		{
			var obstacle = this.obstacles[i];
			obstacle.drawToDisplay(display);
		}

		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.drawToDisplay(display);
		}

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.drawToDisplay(display);
		}
	}
}

// run

main();

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

An Artillery Game in JavaScript

Below is a simple artillery game implemented in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2017/11/22 – I have updated this code to account for explosions being blocked by the landscape, though it may not be immediately clear that this is happening based on the visuals.

I also plan to make a live version of this game available at the URL “https://thiscouldbebetter.neocities.org/artillerygame.html“, and to post a Git repository of the code at https://github.com/thiscouldbebetter/ArtilleryGame“.

ArtilleryGame.png


<html>
<body>
<script type="text/javascript">
 
// main

function main()
{
	var displaySize = new Coords(200, 200);
 
	var display = new Display(displaySize);
 
	var world = World.random(new Coords(0, .05), displaySize);
 
	Globals.Instance.initialize
	(
		10, // ticksPerSecond
		display,
		world
	);
}
 
// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.clone = function()
	{
		var returnValues = [];

		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var elementCloned = element.clone();
			returnValues.push(elementCloned);
		}

		return returnValues;
	}
}
 
// classes
 
function Activity(perform)
{
	this.perform = perform;
}
{
	Activity.Instances = new Activity_Instances()
	 
	function Activity_Instances()
	{
		this.DoNothing = new Activity(function perform() {});
		this.UserInputAccept = new Activity
		(
			function perform(world, actor)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var inputActive = inputHelper.keyPressed;
				var powerFactor = 1000;
 
				if (inputActive == "ArrowDown")
				{
					actor.powerCurrent -= actor.powerPerTick;
					actor.powerCurrent = 
						Math.round(actor.powerCurrent * powerFactor) / powerFactor;
					if (actor.powerCurrent < actor.powerMin)
					{
						actor.powerCurrent = actor.powerMin;
					}
				}   
				else if (inputActive == "ArrowLeft")
				{
					actor.firePolar.azimuthInTurns -= actor.turnsPerTick;
					actor.firePolar.trimAzimuthToRangeMinMax
					(
						actor.azimuthInTurnsMin, 
						actor.azimuthInTurnsMax
					);
				}
				else if (inputActive == "ArrowRight")
				{
					actor.firePolar.azimuthInTurns += actor.turnsPerTick;
					actor.firePolar.trimAzimuthToRangeMinMax
					(
						actor.azimuthInTurnsMin, 
						actor.azimuthInTurnsMax
					);
				}
				else if (inputActive == "ArrowUp")
				{
					actor.powerCurrent += actor.powerPerTick;
					actor.powerCurrent = 
						Math.round(actor.powerCurrent * powerFactor) / powerFactor;
					if (actor.powerCurrent > actor.powerMax)
					{
						actor.powerCurrent = actor.powerMax;
					}
				}
				else if (inputActive == "Enter")
				{
					var projectile = new Projectile
					(
						actor.color,
						actor.muzzlePos.clone(),
						// vel
						actor.firePolar.toCoords
						(
							new Coords()
						).normalize().multiplyScalar
						(
							actor.powerCurrent
						)
					);
 
					world.projectiles = [ projectile ];
 
					world.actorIndexCurrent = 1 - world.actorIndexCurrent;
				}   
				inputHelper.keyPressed = false;
			}
		);
	}
}
 
function Actor(color, pos, activity)
{
	this.color = color;
	this.pos = pos;
	this.activity = activity;
 
	this.wins = 0;
	this.ticksSinceKilled = null;
	this.ticksToDie = 30;
 
	this.collider = new Circle(this.pos, 8);
 
	this.powerMin = 1;
	this.powerMax = 4;
	this.powerPerTick = .1;
 
	this.azimuthInTurnsMin = .5;
	this.azimuthInTurnsMax = 1;
	this.turnsPerTick = 1.0 / Polar.DegreesPerTurn;
	this.firePolar = new Polar
	(
		(this.azimuthInTurnsMin + this.azimuthInTurnsMax) / 2, 
		this.collider.radius * 2
	);
	this.muzzlePos = this.pos.clone().add
	(
		this.firePolar.toCoords( new Coords() )
	);
 
	this.vel = new Coords();
 
	this.reset();
}
{
	Actor.prototype.reset = function()
	{
		this.firePolar.azimuthInTurns = 
			(this.azimuthInTurnsMin + this.azimuthInTurnsMax) / 2, 
		this.powerCurrent = (this.powerMin + this.powerMax) / 2;
 
		this.ticksSinceKilled = null;
		this.pos.y = 0;
		this.vel.clear();
	}
 
	Actor.prototype.updateForTimerTick = function(world)
	{
		if (this.ticksSinceKilled == null)
		{
			if (this == world.actorCurrent() && world.projectiles.length == 0)
			{
				this.activity.perform(world, this);
			}
 
			this.firePolar.toCoords(this.muzzlePos);
			this.muzzlePos.add(this.pos);
 
			var surfaceAltitude = world.landscape.altitudeAtX
			(
				this.pos.x
			);
			var isBelowGround = (this.pos.y >= surfaceAltitude);
			if (isBelowGround == false)
			{
				this.vel.add(world.gravityPerTick);
				this.pos.add(this.vel);
			}
			else
			{
				this.vel.clear();
				this.pos.y = surfaceAltitude;
			}
 
		}
		else if (this.ticksSinceKilled < this.ticksToDie)
		{
			 this.ticksSinceKilled++;
		}
		else
		{
			world.reset();
		}
	}
 
	// drawable
 
	Actor.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.collider.radius, this.color);
		display.drawLine
		(
			this.pos,
			this.muzzlePos
		);
		 
		if (this == Globals.Instance.world.actorCurrent())
		{
			var fireAzimuthInTurnsRecentered = Math.abs
			( 
				0.75 - this.firePolar.azimuthInTurns
			);
			var fireAzimuthInDegrees = Math.round
			(
				fireAzimuthInTurnsRecentered 
				* Polar.DegreesPerTurn
			);
			var text = "Angle:" + fireAzimuthInDegrees + " Power:" + this.powerCurrent;
			display.drawText
			(
				text,
				this.collider.radius,
				Coords.Instances.Zeroes,
				this.color
			);
		}
	}
}

function Circle(center, radius)
{
	this.center = center;
	this.radius = radius;
}

function CollisionHelper()
{
	this.displacement = new Coords();
}
{
	CollisionHelper.Instance = new CollisionHelper();
 
	CollisionHelper.prototype.doCirclesCollide = function(circle0, circle1)
	{
		var distanceBetweenCenters = this.displacement.overwriteWith
		(
			circle1.center
		).subtract
		(
			circle0.center
		).magnitude();
 
		var sumOfRadii = circle0.radius + circle1.radius;
 
		var returnValue = (distanceBetweenCenters < sumOfRadii);
 
		return returnValue;
	}

	CollisionHelper.prototype.doEdgesCollide = function(edge0, edge1)
	{
		var returnValue = null;
 
		if (this.edgeProjected == null)
		{
			this.edgeProjected = new Edge([new Coords(), new Coords()]);
		}
		var edgeProjected = this.edgeProjected;
		edgeProjected.overwriteWith(edge1).projectOnto(edge0);
		var edgeProjectedStart = edgeProjected.vertices[0];
		var edgeProjectedDirection = edgeProjected.direction;	   
 
		var distanceAlongEdgeProjectedToXAxis = 
			0 - edgeProjectedStart.y
			/ edgeProjectedDirection.y
 
		if 
		(
			distanceAlongEdgeProjectedToXAxis > 0 
			&& distanceAlongEdgeProjectedToXAxis < edgeProjected.length
		)
		{
			var distanceAlongEdge0ToIntersection =
				edgeProjectedStart.x 
				+ (edgeProjectedDirection.x * distanceAlongEdgeProjectedToXAxis);
 
			if 
			(
				distanceAlongEdge0ToIntersection > 0
				&& distanceAlongEdge0ToIntersection < edge0.length
			)
			{
				returnValue = true;
			}
		}
 
		return returnValue;
	}
}
 
function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();
 
	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}
 
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.addXY = function(x, y)
	{
		this.x += x;
		this.y += y;
		return this;
	}
 
	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;
	}
 
	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.dotProduct = function(other)
	{
		return this.x * other.x + this.y * other.y;
	}
 
	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}
 
	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;   
		return this;
	}
 
	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

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

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

	Coords.prototype.right = function()
	{
		var temp = this.x;
		this.x = 0 - this.y;
		this.y = temp;
		return this;
	}
	
	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}
}
 
function Display(size)
{
	this.size = size;
 
	this.colorBack = "White";
	this.colorFore = "Gray";
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		this.graphics = canvas.getContext("2d");
 
		document.body.appendChild(canvas);
	}
 
	// drawing
 
	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = this.colorBack;
		this.graphics.fillRect
		(
			0, 0, this.size.x, this.size.y
		);
 
		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);
	}
 
	Display.prototype.drawCircle = function(center, radius, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;
 
		this.graphics.beginPath();
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);
		this.graphics.stroke();
	}
 
	Display.prototype.drawLine = function(fromPos, toPos, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}
 
	Display.prototype.drawRectangle = function(pos, size, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;
		this.graphics.strokeRect
		(
			pos.x, pos.y, size.x, size.y
		);
	}
 
	Display.prototype.drawText = function(text, height, pos, color)
	{
		this.graphics.strokeStyle = this.colorBack;
		this.graphics.strokeText(text, pos.x, pos.y + height);
 
		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y + height);
	}
}

function Edge(vertices)
{
	this.vertices = vertices;

	this.displacement = new Coords();
	this.direction = new Coords();
	this.right = new Coords();
	this.recalculateDerivedValues();
}
{
	Edge.prototype.clone = function()
	{
		return new Edge(this.vertices.clone());
	}

	Edge.prototype.overwriteWith = function(other)
	{
		this.vertices[0].overwriteWith(other.vertices[0]);
		this.vertices[1].overwriteWith(other.vertices[1]);
		this.recalculateDerivedValues();
		return this;
	}

	Edge.prototype.projectOnto = function(other)
	{
		for (var i = 0; i < this.vertices.length; i++)
		{
			var vertexThis = this.vertices[i];
 
			vertexThis.subtract
			(
				other.vertices[0]
			).overwriteWithXY
			(
				vertexThis.dotProduct(other.direction),
				vertexThis.dotProduct(other.right)
			);
		}
 
		this.recalculateDerivedValues();
 
		return this;
	}

	Edge.prototype.recalculateDerivedValues = function()
	{
		this.displacement.overwriteWith
		(
			this.vertices[1]
		).subtract(this.vertices[0]);
		this.length = this.displacement.magnitude();
		this.direction.overwriteWith(this.displacement).divideScalar(this.length);
		this.right.overwriteWith(this.direction).right();
	}
}
 
function Globals()
{
	// Do nothing.
}
{
	Globals.Instance = new Globals();
 
	Globals.prototype.initialize = function(timerTicksPerSecond, display, world)
	{
		this.display = display;
		this.display.initialize();
 
		this.world = world;
 
		this.inputHelper = new InputHelper();
		 
		var millisecondsPerTimerTick = Math.floor(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);
 
		this.inputHelper.initialize();
	}
 
	// events
 
	Globals.prototype.handleEventTimerTick = function()
	{
		this.world.drawToDisplay(this.display);
		this.world.updateForTimerTick();
	}
}
 
function InputHelper()
{
	this.keyPressed = null;
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
	}
 
	// events 
 
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.keyPressed = event.key;
	}
}
 
function Landscape(size, horizonPoints)
{
	this.size = size;
	this.color = "Green";   

	this.edges = [];
	var horizonPointPrev = horizonPoints[0];
	for (var i = 1; i < horizonPoints.length; i++)
	{
		var horizonPoint = horizonPoints[i];
		var edge = new Edge([horizonPointPrev, horizonPoint]);
		this.edges.push(edge);
		horizonPointPrev = horizonPoint;
	}
}
{
	Landscape.random = function(size, numberOfPoints)
	{
		var points = [];
		for (var i = 0; i < numberOfPoints + 1; i++)
		{
			var point = new Coords(i * size.x / numberOfPoints, 0);
			points.push(point);
		}
 
		var returnValue = new Landscape(size, points).randomize();
 
		return returnValue;
	}
 
	// instance methods
 
	Landscape.prototype.altitudeAtX = function(xToCheck)
	{
		var returnValue;
 
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			var horizonPointPrev = edge.vertices[0];
			var horizonPoint = edge.vertices[1];

			if (horizonPoint.x > xToCheck)
			{
				var horizonChange = horizonPoint.clone().subtract
				(
					horizonPointPrev
				);
	 
				var t = 
					(xToCheck - horizonPointPrev.x)
					/ (horizonChange.x);
 
				var altitude = 
					horizonPointPrev.y 
					+ (t * horizonChange.y);
 
				returnValue = altitude;
				break;
			}
 
			horizonPointPrev = horizonPoint;
		}
 
		return returnValue;		 
	}

	Landscape.prototype.collidesWithEdge = function(edgeOther)
	{
		var returnValue = false;
		var collisionHelper = CollisionHelper.Instance;
		for (var i = 0; i < this.edges.length; i++)
		{
			var edgeThis = this.edges[i];
			var doEdgesCollide = collisionHelper.doEdgesCollide
			(
				edgeThis, edgeOther
			);
			if (doEdgesCollide == true)
			{
				returnValue = true;
				break;
			}
		}
		return returnValue
	}
 
	Landscape.prototype.randomize = function()
	{
		var altitudeMid = this.size.y / 2;
		var altitudeRange = this.size.y / 2;
		var altitudeRangeHalf = altitudeRange / 2;
		var altitudeMin = altitudeMid - altitudeRangeHalf;
		var altitudeMax = altitudeMin + altitudeRange;
 
		this.edges[0].vertices[0].y = 
			altitudeMin + Math.random() * altitudeRange;
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			var point = edge.vertices[1];
			point.y = altitudeMin + Math.random() * altitudeRange;
			edge.recalculateDerivedValues();
		}
 
		return this;		
	}
 
	// drawable
	 
	Landscape.prototype.drawToDisplay = function(display)
	{
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			display.drawLine(edge.vertices[0], edge.vertices[1], this.color);
		}
	}
}
 
function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2;
	Polar.DegreesPerTurn = 360;
 
	Polar.prototype.toCoords = function(coords)
	{
		var azimuthInRadians = this.azimuthInTurns * Polar.RadiansPerTurn; 
		coords.x = Math.cos(azimuthInRadians) * this.radius;
		coords.y = Math.sin(azimuthInRadians) * this.radius;
		return coords;
	}
 
	Polar.prototype.trimAzimuthToRangeMinMax = function(min, max)
	{
		if (this.azimuthInTurns < min)
		{
			this.azimuthInTurns = min;
		}
		else if (this.azimuthInTurns > max)
		{
			this.azimuthInTurns = max;
		}
		return this;
	}
}
 
function Projectile(color, pos, vel)
{
	this.color = color;
	this.pos = pos;
	this.vel = vel;
 
	this.collider = new Circle(this.pos, 2);
 
	this.ticksSinceExplosion = null;
	this.ticksToExplode = 30;
	this.radiusExplodingMax = 20;
}
{
	Projectile.prototype.radiusCurrent = function()
	{
		var radiusCurrent = 
			this.radiusExplodingMax 
			* this.ticksSinceExplosion 
			/ this.ticksToExplode;

		return radiusCurrent;
	}
 
	Projectile.prototype.updateForTimerTick = function(world)
	{
		if (this.ticksSinceExplosion == null)
		{
			this.vel.add(world.gravityPerTick);
			this.pos.add(this.vel);
			if (this.pos.y > world.size.y)
			{
				world.projectiles.length = 0;
			}
			else
			{
				var surfaceAltitude = world.landscape.altitudeAtX(this.pos.x);
				var isBeneathHorizon = (this.pos.y >= surfaceAltitude);
				if (isBeneathHorizon == true)
				{
					this.ticksSinceExplosion = 0;
					this.pos.y = surfaceAltitude;
				}
			}
		}
		else if (this.ticksSinceExplosion < this.ticksToExplode)
		{
			this.ticksSinceExplosion++;
		}
		else
		{  
			var collisionHelper = CollisionHelper.Instance;
			var actors = world.actors;
			for (var i = 0; i < actors.length; i++)
			{
				var actor = actors[i];

				this.collider.radius = this.radiusCurrent();
				var isActorWithinExplosionRadius = collisionHelper.doCirclesCollide
				(
					this.collider, actor.collider
				);

				if (isActorWithinExplosionRadius == true)
				{
					var edgeFromExplosionToActor = new Edge
					([
						this.pos.clone().addXY(0, -1), // hack 
						actor.pos.clone().addXY(0, -1)
					]);
					var isExplosionBlockedByGround = world.landscape.collidesWithEdge
					(
						edgeFromExplosionToActor
					);

					if (isExplosionBlockedByGround == false)
					{
						var actorOther = actors[1 - i];
						actorOther.ticksSinceKilled = 0;
						actorOther.wins++;
					}
				}
			}
			world.projectiles.length = 0;
		}
	}

	// drawable

	Projectile.prototype.drawToDisplay = function(display)
	{
		if (this.ticksSinceExplosion == null)
		{
			display.drawCircle
			(
				this.pos, this.collider.radius, this.color
			);
			display.drawLine
			(
				this.pos, 
				this.pos.clone().subtract(this.vel), 
				this.color
			);
		}
		else
		{
			display.drawCircle(this.pos, this.radiusCurrent(), this.color);
		}
	}
}
 
function World(gravityPerTick, size, landscape, actors)
{
	this.gravityPerTick = gravityPerTick;
	this.size = size;
	this.landscape = landscape;
	this.actors = actors;
 
	this.actorIndexCurrent = 0;
	this.projectiles = [];
}
{
	World.random = function(gravityPerTick, size)
	{
		var landscape = Landscape.random(size, 10);
 
		var actors = 
		[
			new Actor
			(
				"Blue", 
				new Coords(size.x / 6, 0),
				Activity.Instances.UserInputAccept
			), 
			new Actor
			(
				"Red", 
				new Coords(5 * size.x / 6, 0),
				Activity.Instances.UserInputAccept
			), 
		];
 
		var returnValue = new World
		(
			gravityPerTick,
			size,
			landscape,
			actors
		);
 
		return returnValue;
	}
 
	// instance methods
 
	World.prototype.actorCurrent = function()
	{
		return this.actors[this.actorIndexCurrent];
	}
 
	World.prototype.actorCurrentAdvance = function()
	{
		this.actorIndexCurrent = this.actors.length - 1 - this.actorIndexCurrent;
	}
 
	World.prototype.reset = function()
	{
		this.landscape.randomize();
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.reset();
		}   
	}
 
	World.prototype.updateForTimerTick = function()
	{
		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.updateForTimerTick(this);
		}
 
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.updateForTimerTick(this);
		}
	}
 
	// drawable
 
	World.prototype.drawToDisplay = function(display)
	{
		display.clear();
		this.landscape.drawToDisplay(display);
 
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.drawToDisplay(display);
			display.drawText("" + actor.wins, actor.radius, actor.pos, actor.color);
		}
 
		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.drawToDisplay(display);
		}
	}
}
 
// run
 
main();
 
</script>
</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

A Retirement Savings Calculator in JavaScript

The JavaScript program below, when run, allows a user to calculate how many years it will take to save enough to replace their salary based on the specified initial savings, savings per year, and rates of return before and after retirement. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/retirementsavingscalculator.html.

RetirementSavingsCalculator.png


 <div id="divUI">

	<p><b>Retirement Savings Calculator</b></p>

	<label>Initial Savings:</label><br />
	<input id="inputSavingsInitial" type="number" value="0"></input><br />
	
	<label>Return on Investment per Period Before Retirement:</label><br />
	<input id="inputReturnOnInvestmentPerPeriodBeforeRetirement" type="number" value=".1"></input><br />

	<label>New Savings per Period:</label><br />
	<input id="inputSavingsNewPerPeriod" type="number" value=".5"></input><br />

	<label>Return on Investment per Period After Retirement:</label><br />
	<input id="inputReturnOnInvestmentPerPeriodAfterRetirement" type="number" value=".05"></input><br />

	<label>Retirement Income Desired per Period:</label><br />
	<input id="inputRetirementIncomePerPeriod" value=".5"></input><br />

	<button onclick="buttonCalculate_Clicked();">Calculate</button><br />

	<label>Savings at Retirement:</label><br />
	<input id="inputSavingsAtRetirement" disabled="true"></input><br />

	<label>Periods Required:</label><br />
	<input id="inputPeriodsRequired" disabled="true"></input><br />

</div>

<script type="text/javascript">

/*

Derivation of the "Savings Plan Equation":

n = number of periods (usually years)
s_n = savings after n periods
d = new savings per period
r = 1 + return on investment per period

Start with the first two periods:

s1 = d + s0r
s2 = d + s1r
   = d + (d + s0r)r
   = d + dr + s0r^2
   = d(1 + r) + s0r^2

Extrapolate from n = 2 to find the pattern for any n:

s_n = d(1 + r + r^2 + ... + r^(n-1)) + s0r^n

Multiply each term in the sequence ( 1 + ... r^(n-1) ) 
by (r - 1)/(r - 1).  
All but the first and last terms 
in the resulting sequence cancel:

s_n = d ( (r^n - 1) / (r - 1) ) + s0r^n

Now solve for n to find 
the number of periods required to get from s0 to s_n:

s_n = d ( (r^n - 1) / (r-1) ) + s0r^n
s_n = d ( (r^n - 1) / (r-1) ) + ( s0r^n(r-1) / (r-1) )
s_n = d ( (r^n - 1) / (r-1) ) + ( s0r^(n+1) -s0r^n ) / (r-1)
s_n = (dr^n - d) / (r-1) + ( s0r^(n+1) -s0r^n ) / (r-1)
s_n = ( dr^n - d + s0r^(n+1) -s0r^n ) / (r-1)
s_n * (r - 1) = dr^n - d + s0r^(n+1) - s0r^n
s_n * (r - 1) + d = dr^n + s0r^(n+1) - s0r^n
s_n * (r - 1) + d = r^n(d + s0r - s0)
(s_n * (r - 1) + d) / (d + s0r - s0) = r^n
log_r ( (s_n * (r - 1) + d) / (d + s0r - s0) ) = log_r r^n
log_r ( (s_n * (r - 1) + d) / (d + s0r - s0) ) = n
n = log_r ( (s_n * (r - 1) + d) / (d + s0r - s0) )

Change the logarithm's base from r to 10:
log_a x = log_b x / log_b a

n = log ( (s_n * (r - 1) + d) / (d + s0r - s0) ) / log r
n = log ( (s_n * (r - 1) + d) / (d + s0(r - 1)) ) / log r

*/

// ui event handlers

function buttonCalculate_Clicked()
{
	var inputSavingsInitial
		= document.getElementById("inputSavingsInitial");
	var inputReturnOnInvestmentPerPeriodBeforeRetirement
		= document.getElementById("inputReturnOnInvestmentPerPeriodBeforeRetirement");
	var inputSavingsNewPerPeriod
		= document.getElementById("inputSavingsNewPerPeriod");
	var inputReturnOnInvestmentPerPeriodAfterRetirement
		= document.getElementById("inputReturnOnInvestmentPerPeriodAfterRetirement");
	var inputRetirementIncomePerPeriod
		= document.getElementById("inputRetirementIncomePerPeriod");
	var inputPeriodsRequired 
		= document.getElementById("inputPeriodsRequired");

	var savingsInitial = 
		parseFloat(inputSavingsInitial.value);
	var returnOnInvestmentPerPeriodBeforeRetirement = 
		parseFloat(inputReturnOnInvestmentPerPeriodBeforeRetirement.value);
	var savingsNewPerPeriod = 
		parseFloat(inputSavingsNewPerPeriod.value);
	var returnOnInvestmentPerPeriodAfterRetirement = 
		parseFloat(inputReturnOnInvestmentPerPeriodAfterRetirement.value);
	var retirementIncomePerPeriod = 
		parseFloat(inputRetirementIncomePerPeriod.value);
	
	var savingsAtRetirement = 
		retirementIncomePerPeriod 
		/ returnOnInvestmentPerPeriodAfterRetirement;
	inputSavingsAtRetirement.value = savingsAtRetirement;

	var periodsRequired = Math.log
	(
		( 
			savingsAtRetirement 
			* returnOnInvestmentPerPeriodBeforeRetirement
			+ savingsNewPerPeriod 
		)
		/ 
		(
			savingsNewPerPeriod
			+ savingsInitial
			* returnOnInvestmentPerPeriodBeforeRetirement
		) 		
	)
	/ Math.log(1 + returnOnInvestmentPerPeriodBeforeRetirement);

	inputPeriodsRequired.value = periodsRequired;
}

</script>

Posted in Uncategorized | Tagged , , , , , | Leave a comment

An Adding Machine in JavaScript

The JavaScript code below implements a simple adding machine. To see it in action, copy it into a .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/addingmachine.html.

AddingMachine.png


<html>
<body>

<div id="divUI">

	<label>Addends:</label><br />	
	<textarea id="textareaAddends" cols="80" rows="25">
1
2
fizz
4
buzz

42 (The answer to everything.)
<=(1,337)=>
0.999999999
-40 (Celsius or Fahrenheit?)
...
How many licks does it take? 3, that's how many.
	</textarea><br />
	<button onclick="buttonAdd_Clicked();">Add</button><br />
	<label>Sum:</label><br />
	<input id="inputSum"></input><br />
	
</div>

<script type="text/javascript">

// ui event handlers

function buttonAdd_Clicked()
{
	var textareaAddends = document.getElementById("textareaAddends");

	var addendsAsString = textareaAddends.value;
	var addendsAsLines = addendsAsString.split("\n");

	var charsToKeep = "0123456789.-";

	var sumSoFar = 0;
	for (var i = 0; i < addendsAsLines.length; i++)
	{
		var addendAsLine = addendsAsLines[i];
		var addendTrimmed = "";
		for (var j = 0; j < addendAsLine.length; j++)
		{
			var addendChar = addendAsLine[j];
			if (charsToKeep.indexOf(addendChar) >= 0)
			{
				addendTrimmed += addendChar;
			}
		}
		var addendAsNumber = parseFloat(addendTrimmed);
		if (isNaN(addendAsNumber) == false)
		{
			sumSoFar += addendAsNumber;
		}
	}
	
	var inputSum = document.getElementById("inputSum");
	inputSum.value = sumSoFar;	
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , , , | Leave a comment

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>
Posted in Uncategorized | Tagged , , , , , | Leave a comment

A Simple Data Transcoder in JavaScript

The JavaScript program below converts data encoded in UTF8, Base64, or hexadecimal format into one of the other encodings. To see the code in action, copy it into a .html file and open that file in a text encoder that runs JavaScript. Or, for an online version, visit “https://thiscouldbebetter.neocities.org/datatranscoder.html”.

DataTranscoder.png


<html>
<body>

<div id="divUI">

	<label><b>Data Transcoder</b></label><br />

	<label>Data to Encode:</label><br />
	<textarea id="textareaDataToEncode" cols="40" rows="10"></textarea><br />

	<label>From:</label>
	<select id="selectEncodingFrom">
		<option>Base64</option>
		<option>Hexadecimal</option>
		<option selected>UTF8</option>
	</select>
	<label>To:</label>
	<select id="selectEncodingTo">
		<option selected>Base64</option>
		<option>Hexadecimal</option>
		<option>UTF8</option>
	</select><br />

	<button onclick="buttonEncode_Clicked();">v Encode v</button>
	<button onclick="buttonDecode_Clicked();">^ Decode ^</button><br />

	<label>Data Encoded:</label><br />
	<textarea id="textareaDataEncoded" cols="40" rows="10"></textarea><br />

</div>

<script type="text/javascript">

// ui event handlers

function buttonDecode_Clicked()
{
	var selectEncodingFrom = document.getElementById
	(
		"selectEncodingFrom"
	);

	var selectEncodingTo = document.getElementById
	(
		"selectEncodingTo"
	);

	var textareaDataToEncode = document.getElementById
	(
		"textareaDataToEncode"
	);

	var textareaDataEncoded = document.getElementById
	(
		"textareaDataEncoded"
	);

	var encodingNameFrom = selectEncodingFrom.value;
	var encodingFrom = Encoding.Instances[encodingNameFrom];

	var encodingNameTo = selectEncodingTo.value;
	var encodingTo = Encoding.Instances[encodingNameTo];

	var dataToEncode = textareaDataEncoded.value;
	var dataAsBytes = encodingTo.decodeToBytes(dataToEncode);
	var dataEncoded = encodingFrom.encodeBytes(dataAsBytes);

	textareaDataToEncode.value = dataEncoded;
}

function buttonEncode_Clicked()
{
	var selectEncodingFrom = document.getElementById
	(
		"selectEncodingFrom"
	);

	var selectEncodingTo = document.getElementById
	(
		"selectEncodingTo"
	);

	var textareaDataToEncode = document.getElementById
	(
		"textareaDataToEncode"
	);

	var textareaDataEncoded = document.getElementById
	(
		"textareaDataEncoded"
	);

	var encodingNameFrom = selectEncodingFrom.value;
	var encodingFrom = Encoding.Instances[encodingNameFrom];

	var encodingNameTo = selectEncodingTo.value;
	var encodingTo = Encoding.Instances[encodingNameTo];

	var dataToEncode = textareaDataToEncode.value;
	var dataAsBytes = encodingFrom.decodeToBytes(dataToEncode);
	var dataEncoded = encodingTo.encodeBytes(dataAsBytes);

	textareaDataEncoded.value = dataEncoded;
}

// classes

function Encoding(name, decodeToBytes, encodeBytes)
{
	this.name = name;
	this.decodeToBytes = decodeToBytes;
	this.encodeBytes = encodeBytes;
}
{
	Encoding.Instances = new Encoding_Instances();

	function Encoding_Instances()
	{
		this.Base64 = new Encoding
		(
			"Base64",
			function decodeToBytes(dataToDecode)
			{
				var dataAsBinaryString = atob(dataToDecode);
				var dataAsBytes = [];
				for (var i = 0; i < dataAsBinaryString.length; i++)
				{
					var byte = dataAsBinaryString.charCodeAt(i);
					dataAsBytes.push(byte);
				}
				return dataAsBytes;
			},
			function encodeBytes(bytesToEncode)
			{
				var dataAsBinaryString = "";
				for (var i = 0; i < bytesToEncode.length; i++)
				{
					var byte = bytesToEncode[i];
					var byteAsChar = String.fromCharCode(byte);
					dataAsBinaryString += byteAsChar;
				}
				var returnValue = btoa(dataAsBinaryString);
				return returnValue;
			}
		);

		this.Hexadecimal = new Encoding
		(
			"Hexadecimal",
			function decodeToBytes(dataToDecode)
			{
				var returnValues = []
				for (var i = 0; i < dataToDecode.length; i += 2)
				{
					var byteAsHexadecimal = dataToDecode.substr(i, 2);
					var byte = parseInt(byteAsHexadecimal, 16);
					returnValues.push(byte);
				}
				return returnValues;
			},
			function encodeBytes(bytesToEncode)
			{
				var returnValue = "";

				for (var i = 0; i < bytesToEncode.length; i++)
				{
					var byte = bytesToEncode[i];
					var byteAsString = byte.toString(16);
					returnValue += byteAsString;
				}

				return returnValue;
			}
		);

		this.UTF8 = new Encoding
		(
			"UTF8",
			function decodeToBytes(dataToDecode)
			{
				var returnValues = []
				for (var i = 0; i < dataToDecode.length; i++)
				{
					var byte = dataToDecode.charCodeAt(i);
					returnValues.push(byte);
				}
				return returnValues;

			},
			function encodeBytes(bytesToEncode)
			{
				var returnValue = "";

				for (var i = 0; i < bytesToEncode.length; i++)
				{
					var byte = bytesToEncode[i];
					var char = String.fromCharCode(byte);
					returnValue += char;
				}
				return returnValue;
			}
		);
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment