A Tetris Clone in JavaScript

The JavaScript code below implements a simple clone of Tetris in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or you can play an online version by visiting http://thiscouldbebetter.neocities.org/tetris.html.

Use the A and D keys to move the current block back and forth, the W key to rotate it, and the S key to cause it to fall faster.

I’ve always considered Tetris as something of a Zen exercise, and this version of the game is especially Zen, since in its current revision it doesn’t keep track of lines completed, and it never speeds up.  Om…

Tetris

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

// main

function main()
{
	var blockDefnsAll = BlockDefn.Instances._All;

	var level0 = new Level
	(
		5, // fallPeriodOfBlocksInTicksPerCell
		new Map(new Coords(10, 20))
	);

	var world0 = new World
	(
		blockDefnsAll,
		level0
	);

	Globals.Instance.initialize
	(
		100, // millisecondsPerTimerTick
		new Coords(160, 320), // viewSize
		world0
	);
}

// classes

function Block(defn, posInCells)
{
	this.defn = defn;
	this.posInCells = posInCells;
	this.orientation = new Coords(1, 0);

	this.cellPositionsOccupied = 
	[
		new Coords(0, 0),
		new Coords(0, 0),
		new Coords(0, 0),
		new Coords(0, 0),
	];

	this.cellPositionsOccupiedUpdate();
}
{
	// instance methods

	Block.prototype.cellPositionsOccupiedUpdate = function()
	{
		var defn = this.defn;

		for (var i = 0; i < defn.offsets.length; i++)
		{
			var offset = defn.offsets[i];
			var posToOverwrite = this.cellPositionsOccupied[i];
			
			posToOverwrite.overwriteWith
			(
				offset
			).orient
			(
				this.orientation
			).add
			(
				this.posInCells
			)
		}
		
	}

	Block.prototype.copyCellsOccupiedToMap = function(map)
	{
		var returnValue = false;

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

			map.setCellAtPosAsOccupied(cellPos);
		}
	}

	Block.prototype.collidesWithMapBottom = function(map)
	{
		var returnValue = false;

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

			if (cellPos.y >= map.sizeInCells.y)
			{
				returnValue = true;
				break;
			}	
		}

		return returnValue;	
	}

	Block.prototype.collidesWithMapCellsOccupied = function(map)
	{
		var returnValue = false;

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

			var isCellAtPosOccupied = map.isCellAtPosOccupied
			(
				cellPos
			);

			if (isCellAtPosOccupied == true)
			{
				returnValue = true;
				break;
			}
		}	

		return returnValue;	
	}

	Block.prototype.collidesWithMapSides = function(map)
	{
		var returnValue = false;

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

			if (cellPos.x < 0 || cellPos.x >= map.sizeInCells.x)
			{
				returnValue = true;
				break;
			}		
		}

		return returnValue;	
	}

	Block.prototype.collidesWithMapTop = function(map)
	{
		var returnValue = false;

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

			if (cellPos.y < 0)
			{
				returnValue = true;
				break;
			}	
		}

		return returnValue;	
	}

}

function BlockDefn(name, offsets)
{
	this.name = name;	
	this.offsets = offsets;
}
{
	BlockDefn.Instances = new BlockDefn_Instances();

	function BlockDefn_Instances()
	{
		this.Aye = new BlockDefn("Aye", [new Coords(-1, 0), new Coords(0, 0), new Coords(1, 0), new Coords(2, 0)]);
		this.Ell = new BlockDefn("Ell", [new Coords(-1, 1), new Coords(-1, 0), new Coords(0, 0), new Coords(1, 0)]);
		this.Ess = new BlockDefn("Ess", [new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(-1, 1)]);
		this.Gamma = new BlockDefn("Gamma", [new Coords(-1, 0), new Coords(0, 0), new Coords(1, 0), new Coords(1, 1)]);
		this.Square = new BlockDefn("Square", [new Coords(0, 0), new Coords(1, 0), new Coords(1, 1), new Coords(0, 1)]);
		this.Tee = new BlockDefn("Tee", [new Coords(-1, 0), new Coords(0, 0), new Coords(1, 0), new Coords(0, 1)]);
		this.Zee = new BlockDefn("Zee", [new Coords(-1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1)]);

		this._All = 
		[
			this.Aye,
			this.Ell,
			this.Ess,
			this.Gamma,
			this.Square,
			this.Tee,
			this.Zee,
		];	
	}
}

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.addXY = function(x, y)
	{
		this.x += x;
		this.y += y;

		return this;
	}

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

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

		return this;
	}

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

	Coords.prototype.isWithinRangeX = function(rangeMax)
	{
		var returnValue = 
		(
			(this.x >= 0 && this.x <= rangeMax.x)
		);

		return returnValue;
	}

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

		return this;
	}

	Coords.prototype.orient = function(forward)
	{
		this.overwriteWithXY
		(
			this.dotProduct(forward),
			this.dotProduct(forward.clone().right())
		);
		
		return this;
	}

	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 DisplayHelper()
{
	// do nothing
}
{
	DisplayHelper.prototype.drawBackground = function()
	{
		this.graphics.fillStyle = "White";
		this.graphics.strokeStyle = "Gray";

		this.graphics.fillRect
		(
			0, 0, 
			this.viewSizeInPixels.x, this.viewSizeInPixels.y
		);

		this.graphics.strokeRect
		(
			0, 0, 
			this.viewSizeInPixels.x, this.viewSizeInPixels.y
		);

	}

	DisplayHelper.prototype.drawBlock = function(block)
	{
		var cellPositionsOccupied = block.cellPositionsOccupied;

		for (var i = 0; i < cellPositionsOccupied.length; i++)
		{
			var cellPos = cellPositionsOccupied[i];

			this.drawMapCellAtPos(cellPos);						
		}
	}

	DisplayHelper.prototype.drawMap = function(map)
	{
		for (var y = 0; y < map.sizeInCells.y; y++)
		{
			this.cellPos.y = y;

			for (var x = 0; x < map.sizeInCells.x; x++)
			{
				this.cellPos.x = x;

				var isCellAtPosOccupied = map.isCellAtPosOccupied
				(
					this.cellPos
				);

				if (isCellAtPosOccupied == true)
				{
					this.drawMapCellAtPos(this.cellPos);
				}
			}
		}
	}

	DisplayHelper.prototype.drawMapCellAtPos = function(cellPos)
	{
		this.graphics.fillStyle = "LightGray";
		this.graphics.strokeStyle = "Gray";

		this.drawPos.overwriteWith
		(
			cellPos
		).multiply
		(
			this.mapCellSizeInPixels
		);

		this.graphics.fillRect
		(
			this.drawPos.x,
			this.drawPos.y,
			this.mapCellSizeInPixels.x,
			this.mapCellSizeInPixels.y
		);

		this.graphics.strokeRect
		(
			this.drawPos.x,
			this.drawPos.y,
			this.mapCellSizeInPixels.x,
			this.mapCellSizeInPixels.y
		);
	}

	DisplayHelper.prototype.initialize = function(viewSizeInPixels, mapCellSizeInPixels)
	{
		this.viewSizeInPixels = viewSizeInPixels;
		this.mapCellSizeInPixels = mapCellSizeInPixels;

		this.canvas = document.createElement("canvas");
		this.canvas.width = this.viewSizeInPixels.x;
		this.canvas.height = this.viewSizeInPixels.y;
		
		this.graphics = this.canvas.getContext("2d");

		document.body.appendChild(this.canvas);

		// temporary variables

		this.cellPos = new Coords(0, 0);
		this.drawPos = new Coords(0, 0);
	}
}

function Globals()
{}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function
	(
		millisecondsPerTimerTick, 
		viewSizeInPixels,
		world
	)
	{
		this.displayHelper = new DisplayHelper();
		this.displayHelper.initialize
		(
			viewSizeInPixels,
			viewSizeInPixels.clone().divide
			(
				world.level.map.sizeInCells
			)
		);

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();

		this.world = world;
		this.world.initialize();

		setInterval
		(
			this.handleEventTimerTick.bind(this),
			millisecondsPerTimerTick
		)
	}

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

function InputHelper()
{}
{
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		if (this.hasActionBeenPerformedThisTick == true)
		{
			return;
		}

		var isKeyPressedMappedToAnAction = true;

		var level = Globals.Instance.world.level;
		var blockCurrent = level.blockCurrent;

		if (blockCurrent != null)
		{
			var map = level.map;
			var blockPosInCells = blockCurrent.posInCells;
			var blockPosPrev = blockPosInCells.clone();
			var blockOrientationPrev = blockCurrent.orientation.clone();

			var keyCode = event.keyCode;
	
			if (keyCode == 65) // a - left
			{
				blockPosInCells.addXY
				(
					-1, 0
				);

				blockCurrent.cellPositionsOccupiedUpdate();

				if 
				(
					blockCurrent.collidesWithMapSides(map) == true
					|| blockCurrent.collidesWithMapCellsOccupied(map) == true
				)
				{
					blockPosInCells.overwriteWith(blockPosPrev);	
				}
			}
			else if (keyCode == 68) // d - right
			{
				blockPosInCells.addXY
				(
					1, 0
				);

				blockCurrent.cellPositionsOccupiedUpdate();

				if 
				(
					blockCurrent.collidesWithMapSides(map) == true
					|| blockCurrent.collidesWithMapCellsOccupied(map) == true
				)
				{
					blockPosInCells.overwriteWith(blockPosPrev);	
				}
			}
			else if (keyCode == 83) // s - down
			{
				blockPosInCells.addXY
				(
					0, 1
				);

				blockCurrent.cellPositionsOccupiedUpdate();

				if 
				(
					blockCurrent.collidesWithMapBottom(map) == true
					|| blockCurrent.collidesWithMapCellsOccupied(map) == true
				)
				{
					blockPosInCells.overwriteWith(blockPosPrev);	
				}
			}
			else if (keyCode == 87) // w - rotate
			{
				blockCurrent.orientation.right();

				blockCurrent.cellPositionsOccupiedUpdate();

				if 
				(
					blockCurrent.collidesWithMapBottom(map) == true
					|| blockCurrent.collidesWithMapSides(map) == true
					|| blockCurrent.collidesWithMapCellsOccupied(map) == true
				)
				{
					blockCurrent.orientation.overwriteWith
					(
						blockOrientationPrev
					);	
				}
			}
			else
			{
				isKeyPressedMappedToAnAction = false;
			}
		}

		if (isKeyPressedMappedToAnAction == true)
		{
			this.hasActionBeenPerformedThisTick = true;
		}
	}

	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
	}

	InputHelper.prototype.updateForTimerTick = function()
	{
		this.hasActionBeenPerformedThisTick = false;
	}
}

function Level(fallPeriodOfBlocksInTicksPerCell, map)
{	
	this.fallPeriodOfBlocksInTicksPerCell = fallPeriodOfBlocksInTicksPerCell;
	this.map = map;

	this.ticksSoFar = 0;
	this.blockCurrent = null;
	this.isTerminated = false;

	this.blockPosInCellsPrev = new Coords(0, 0);
}
{
	Level.prototype.blockGenerate = function()
	{
		var blockDefns = Globals.Instance.world.blockDefns;

		var blockDefnIndex = Math.floor
		(
			Math.random() 
			* blockDefns.length
		);

		var blockDefn = blockDefns[blockDefnIndex];

		var returnValue = new Block
		(
			blockDefn,
			new Coords
			(
				this.map.sizeInCells.x / 2,
				-1
			)
		);

		return returnValue;
	}

	Level.prototype.clearFullRows = function()
	{
		var mapSizeInCells = this.map.sizeInCells;

		var cellPos = new Coords(0, 0);

		var y = mapSizeInCells.y - 1;
		while (y >= 0)
		{
			cellPos.y = y;

			var areAllCellsInRowOccupied = true;

			for (var x = 0; x < mapSizeInCells.x; x++)
			{
				cellPos.x = x;

				var isCellOccupied = this.map.isCellAtPosOccupied	
				(
					cellPos
				);

				if (isCellOccupied == false)
				{
					areAllCellsInRowOccupied = false;
					break;
				}
			}

			if (areAllCellsInRowOccupied == true)
			{
				this.map.cellsAsStrings.splice(y, 1);
				this.map.cellsAsStrings.splice
				(
					0, 0, this.map.cellRowBlankAsString
				);
			}
			else
			{
				y--;
			}
		}
	}

	Level.prototype.initialize = function()
	{
		this.blockCurrent = this.blockGenerate();
	}

	Level.prototype.updateForTimerTick = function()
	{	
		this.ticksSoFar++;

		var blockPosInCells = this.blockCurrent.posInCells;		

		this.blockPosInCellsPrev.overwriteWith(blockPosInCells);

		if (this.ticksSoFar % this.fallPeriodOfBlocksInTicksPerCell == 0)
		{
			blockPosInCells.addXY
			(
				0, 1
			);
		}

		this.blockCurrent.cellPositionsOccupiedUpdate();

		var hasBlockComeToRest = false;

		var hasBlockHitBottom = this.blockCurrent.collidesWithMapBottom
		(
			this.map
		);

		if (hasBlockHitBottom == true)
		{
			hasBlockComeToRest = true;
		}
		else
		{
			var doesBlockCollideWithOthers = this.blockCurrent.collidesWithMapCellsOccupied
			(
				this.map
			);

			if (doesBlockCollideWithOthers == true)
			{
				hasBlockComeToRest = true;
			}
		}

		if (hasBlockComeToRest == true)
		{
			blockPosInCells.overwriteWith
			(
				this.blockPosInCellsPrev
			);
			this.blockCurrent.cellPositionsOccupiedUpdate();

			if (this.blockCurrent.collidesWithMapTop(this.map) == true)
			{
				Globals.Instance.world.level = null;
				alert("Game Over");
			}
			else
			{
				this.blockCurrent.copyCellsOccupiedToMap(this.map);
			}

			this.blockCurrent = null;

			this.clearFullRows();
		}

		if (this.blockCurrent == null)
		{
			this.blockCurrent = this.blockGenerate();
		}

		var displayHelper = Globals.Instance.displayHelper;

		displayHelper.drawBackground();
		
		displayHelper.drawMap
		(
			this.map
		);

		displayHelper.drawBlock
		(
			this.blockCurrent
		);
	}
}

function Map(sizeInCells)
{
	this.sizeInCells = sizeInCells;
	this.sizeInCellsMinusOnes = this.sizeInCells.clone().subtract
	(
		new Coords(1, 1)
	);

	this.cellsAsStrings = [];

	this.cellRowBlankAsString = "";

	for (var x = 0; x < this.sizeInCells.x; x++)
	{
		this.cellRowBlankAsString += ".";
	}

	for (var y = 0; y < this.sizeInCells.y; y++)
	{
		this.cellsAsStrings.push(this.cellRowBlankAsString);
	}


}
{
	Map.prototype.isCellAtPosOccupied = function(cellPos)
	{
		var returnValue = false;

		var cellRowAsString = this.cellsAsStrings[cellPos.y];
		if (cellRowAsString != null)
		{
			var codeCharAtPos = cellRowAsString[cellPos.x];

			if (codeCharAtPos != null)
			{
				returnValue = (codeCharAtPos != ".");
			}
		}

		return returnValue;
	}

	Map.prototype.setCellAtPosAsOccupied = function(cellPos)
	{
		var cellRowAsString = this.cellsAsStrings[cellPos.y];

		cellRowAsString = cellRowAsString.substr
		(
			0,
			cellPos.x
		)
		+ "x"
		+ cellRowAsString.substr
		(
			cellPos.x + 1
		);

		this.cellsAsStrings[cellPos.y] = cellRowAsString;
	}
}

function World(blockDefns, level)
{
	this.blockDefns = blockDefns;
	this.level = level;
}
{
	World.prototype.initialize = function()
	{
		this.level.initialize();
	}

	World.prototype.updateForTimerTick = function()
	{
		if (this.level != null)
		{
			this.level.updateForTimerTick();
		}
	}
}

// run

main();

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

This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

One Response to A Tetris Clone in JavaScript

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