Creating the Memory Game

Complete Javascript source code


var ctx = null;

var sprites = null, spritesLoaded = false;
var gameTime = 0, lastFrameTime = 0;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0;

var finishedTime = 0;

var offsetX = 0;
var offsetY = 100;

var grid = [];
var visibleFace = null;
var activeFaces = [];

var gameState = {
	screen	: 'menu',
	mode	: 'easy',
	newBest	: false
};

var mouseState = {
	x	: 0,
	y	: 0,
	click	: null
};

var spriteSheet = {
	src		: "sprites.png",
	spriteW		: 28,
	spriteH		: 28,
	cols		: 10,
	rows		: 6
};

var difficulties = {
	easy	: {
		name		: "Easy",
		bestTime	: 0,
		width		: 4,
		height		: 4,
		menuBox		: [0,0]
	},
	medium : {
		name		: "Medium",
		bestTime	: 0,
		width		: 6,
		height		: 6,
		menuBox		: [0,0]
	},
	hard	: {
		name		: "Hard",
		bestTime	: 0,
		width		: 8,
		height		: 7,
		menuBox		: [0,0]
	}
};

function Face(x, y, spriteX, spriteY)
{
	this.spritePos = [(spriteX * spriteSheet.spriteW),
						(spriteY * spriteSheet.spriteH)];
	this.pos = [(x * spriteSheet.spriteW),
				(y * spriteSheet.spriteH)];
	this.typeID = (spriteY * spriteSheet.cols) + spriteX;
	
	this.currentState	= 'hidden';
	this.stateChanged	= 0;
	this.active		= false;
}
Face.prototype.update = function()
{
	if(this.currentState=='incorrect' &&
		(gameTime - this.stateChanged) > 700)
	{
		this.currentState	= 'hidden';
		this.stateChanged	= gameTime;
		this.active		= false;
	}
};
Face.prototype.click = function()
{
	if(this.currentState=='correct') { return; }
	
	if(visibleFace==this)
	{
		this.currentState	= 'hidden';
		this.stateChanged	= 0;
		visibleFace		= null;
		return;
	}
	
	if(visibleFace==null)
	{
		visibleFace		= this;
		this.currentState	= 'visible';
		this.stateChange 	= gameTime;
	}
	else if(visibleFace.typeID==this.typeID)
	{
		this.currentState	= 'correct';
		this.stateChanged	= gameTime;
		
		visibleFace.currentState	= 'correct';
		visibleFace.stateChanged	= gameTime;
		visibleFace			= null;
		
		checkState();
	}
	else
	{
		this.currentState	= 'incorrect';
		this.stateChanged	= gameTime;
		this.active		= true;
		
		visibleFace.currentState	= 'incorrect';
		visibleFace.stateChanged	= gameTime;
		visibleFace.active		= true;
		
		activeFaces.push(this);
		activeFaces.push(visibleFace);
		
		visibleFace = null;
	}
};

function updateGame()
{
	if(gameState.screen=='menu')
	{
		if(mouseState.click!=null)
		{
			for(var d in difficulties)
			{
				if(mouseState.click[1]>=difficulties[d].menuBox[0] &&
					mouseState.click[1]<=difficulties[d].menuBox[1])
				{
					startLevel(d);
					break;
				}
			}
			mouseState.click = null;
		}
	}
	else if(gameState.screen=='won')
	{
		if(mouseState.click!=null)
		{
			gameState.screen = 'menu';
			mouseState.click = null;
		}
	}
	else
	{
		for(var x in activeFaces) { activeFaces[x].update(); }
		activeFaces = activeFaces.filter(function(e) {
			return e.active;
		});
		
		if(mouseState.click!=null)
		{
			var cDiff = difficulties[gameState.mode];
			
			if(mouseState.click[0]>=offsetX &&
				mouseState.click[1]>=offsetY &&
				mouseState.click[0]<=(cDiff.width*spriteSheet.spriteW)+offsetX &&
				mouseState.click[1]<=(cDiff.height*spriteSheet.spriteH)+offsetY)
			{
				var gridX = Math.floor((mouseState.click[0]-offsetX) /
					spriteSheet.spriteW);
				var gridY = Math.floor((mouseState.click[1]-offsetY) /
					spriteSheet.spriteH);
				
				grid[((gridY*cDiff.width)+gridX)].click();
			}
			else if(mouseState.click[1] >= 380)
			{
				gameState.screen = 'menu';
			}
		}
		mouseState.click = null;
	}
}

function checkState()
{
	var allCorrect = true;
	
	for(var g in grid)
	{
		if(grid[g].currentState!='correct')
		{
			allCorrect = false;
			break;
		}
	}
	
	if(allCorrect)
	{
		gameState.screen = 'won';
		
		if(difficulties[gameState.mode].bestTime==0 ||
			gameTime < difficulties[gameState.mode].bestTime)
		{
			difficulties[gameState.mode].bestTime = gameTime;
			gameState.newBest = true;
		}
		
		finishedTime = gameTime;
	}
}

function startLevel(diff)
{
	gameTime		= 0;
	lastFrameTime		= 0;
	gameState.screen	= 'playing';
	gameState.newBest	= false;
	gameState.mode		= diff;
	visibleFace			= null;

	activeFaces.length	= 0;	
	grid.length 		= 0;
	
	offsetX = Math.floor((document.getElementById('game').width -
			(difficulties[diff].width * spriteSheet.spriteW)) / 2);
	
	offsetY = Math.floor((document.getElementById('game').height -
			(difficulties[diff].height * spriteSheet.spriteH)) / 2);
	
	var faceTypes = [];
	for(var i = 0; i < (spriteSheet.cols * spriteSheet.rows); i++)
	{
		faceTypes.push(i);
	}
	
	var gridPlaces = [];
	for(var i = 0; i < (difficulties[diff].width *
		difficulties[diff].height); i++)
	{
		grid.push(null);
		gridPlaces.push(i);
	}
		
	
	for(var i = 0; i < Math.floor(
		(difficulties[diff].width * difficulties[diff].height) / 2); i++)
	{
		var idxF = Math.floor(Math.random() * faceTypes.length);
		var idxFace = faceTypes[idxF];
		
		for(var f = 0; f < 2; f++)
		{
			var idx = Math.floor(Math.random() * gridPlaces.length);
			var idxPlace = gridPlaces[idx];
		
			grid[idxPlace] = new Face(
				idxPlace % difficulties[diff].width,
				Math.floor(idxPlace / difficulties[diff].width),
				idxFace % spriteSheet.cols,
				Math.floor(idxFace / spriteSheet.cols));
			
			gridPlaces.splice(idx, 1);
		}
		
		faceTypes.splice(idxF, 1);
	}
}

window.onload = function()
{
	ctx = document.getElementById('game').getContext('2d');

	// Event listeners
	document.getElementById('game').addEventListener('click', function(e) {
		var pos = realPos(e.pageX, e.pageY);
		mouseState.click = pos;
	});
	document.getElementById('game').addEventListener('mousemove',
	function(e) {
		var pos = realPos(e.pageX, e.pageY);
		mouseState.x = pos[0];
		mouseState.y = pos[1];
	});
	
	// Load our spritesheet
	sprites = new Image();
	sprites.onerror = function()
	{
		ctx = null;
		alert("Failed loading sprite sheet.");
	};
	sprites.onload = function()
	{
		console.log("Sprites loaded");
		spritesLoaded = true;
	};
	sprites.src = spriteSheet.src;
	
	requestAnimationFrame(drawGame);
};

function drawMenu()
{
	ctx.textAlign = 'center';
	ctx.font = "bold 20pt sans-serif";
	ctx.fillStyle = "#000000";
	
	var y = 100;
	
	for(var d in difficulties)
	{
		var mouseOver = (mouseState.y>=(y-20) && mouseState.y<=(y+10));
		
		if(mouseOver) { ctx.fillStyle = "#000099"; }
		
		difficulties[d].menuBox = [y-20, y+10];
		ctx.fillText(difficulties[d].name, 150, y);
		y+= 80;
		
		if(mouseOver) { ctx.fillStyle = "#000000"; }
	}
	
	var y = 120;
	ctx.font = "italic 12pt sans-serif";
	for(var d in difficulties)
	{
		if(difficulties[d].bestTime==0)
		{
			ctx.fillText("No best time", 150, y);
		}
		else
		{
			var t = difficulties[d].bestTime;
			var bestTime = "";
			if((t/1000)>=60)
			{
				bestTime = Math.floor((t/1000)/60) + ":";
				t = t % (60000);
			}
			bestTime+= Math.floor(t/1000) +
				"." + (t%1000);
			ctx.fillText("Best time   " + bestTime, 150, y);
		}
		y+= 80;
	}
}

function drawPlaying()
{
	ctx.textAlign = 'center';
	ctx.font = "bold 20pt sans-serif";
	ctx.fillStyle = "#000000";
	
	ctx.fillText(difficulties[gameState.mode].name, 150, 25);
	
	ctx.textAlign = 'left';
	ctx.font = "italic 12pt sans-serif";
	var t = gameTime;
	var cTime = "Time ";
	if((t/1000)>=60)
	{
		cTime+= Math.floor((t/1000)/60) + ":";
		t = t % (60000);
	}
	cTime+= (Math.floor(t/1000) < 9 ? "0" : "") + Math.floor(t/1000);
	ctx.fillText(cTime, 100, 45);
	
	ctx.fillStyle = "#dddd00";
	
	for(var g in grid)
	{
		if(grid[g].currentState=='hidden')
		{
			ctx.beginPath();
			ctx.arc(offsetX+grid[g].pos[0] + (spriteSheet.spriteW/2),
				offsetY+grid[g].pos[1] + (spriteSheet.spriteH/2),
				Math.round(spriteSheet.spriteW/2), 0, Math.PI*2);
			ctx.closePath();
			ctx.fill();
		}
		else
		{
			ctx.drawImage(sprites,
				grid[g].spritePos[0], grid[g].spritePos[1],
				spriteSheet.spriteW, spriteSheet.spriteH,
				offsetX+grid[g].pos[0], offsetY+grid[g].pos[1],
				spriteSheet.spriteW, spriteSheet.spriteH);
		}
	}
	
	ctx.fillStyle = "#000000";
	ctx.textAlign = 'right';
	ctx.fillText("<< Back to menu", 290, 390);
}

function drawWon()
{
	ctx.textAlign = 'center';
	ctx.font = "bold 20pt sans-serif";
	ctx.fillStyle = "#000000";
	
	ctx.fillText(difficulties[gameState.mode].name, 150, 100);
	
	ctx.font = "italic 12pt sans-serif";
	var t = finishedTime;
	var cTime = "Completed in ";
	if((t/1000)>=60)
	{
		cTime+= Math.floor((t/1000)/60) + ":";
		t = t % (60000);
	}
	cTime+= (Math.floor(t/1000) < 9 ? "0" : "") + Math.floor(t/1000);
	ctx.fillText(cTime, 150, 120);
	
	if(gameState.newBest)
	{
		ctx.fillText("New best time!", 150, 140);
	}
	else
	{
		t = difficulties[gameState.mode].bestTime;
		cTime = "Best time ";
		if((t/1000)>=60)
		{
			cTime+= Math.floor((t/1000)/60) + ":";
			t = t % (60000);
		}
		cTime+= (Math.floor(t/1000) < 9 ? "0" : "") +
			Math.floor(t/1000);
		ctx.fillText(cTime, 150, 140);
	}
	
	ctx.fillText("(Click to jump to menu)", 150, 200);
}

function drawGame()
{
	if(ctx==null) { return; }
	if(!spritesLoaded) { requestAnimationFrame(drawGame); return; }
	
	// Frame & update related timing
	var currentFrameTime = Date.now();
	if(lastFrameTime==0) { lastFrameTime = currentFrameTime; }
	var timeElapsed = currentFrameTime - lastFrameTime;
	gameTime+= timeElapsed;
	
	// Update game
	updateGame();

	// Frame counting
	var sec = Math.floor(Date.now()/1000);
	if(sec!=currentSecond)
	{
		currentSecond = sec;
		framesLastSecond = frameCount;
		frameCount = 1;
	}
	else { frameCount++; }
	
	// Clear canvas
	ctx.fillStyle = "#ddddee";
	ctx.fillRect(0, 0, 300, 400);
	
	// Draw the current screen
	if(gameState.screen=='won') { drawWon(); }
	else if(gameState.screen=='playing') { drawPlaying(); }
	else if(gameState.screen=='menu') { drawMenu(); }
	
	// Draw the frame count
	ctx.textAlign = "left";
	ctx.font = "10pt sans-serif";
	ctx.fillStyle = "#000000";
	ctx.fillText("Frames: " + framesLastSecond, 5, 15);
	
	// Update the lastFrameTime
	lastFrameTime = currentFrameTime;
	
	// Wait for the next frame...
	requestAnimationFrame(drawGame);
}

function realPos(x, y)
{
	var p = document.getElementById('game');
	
	do {
		x-= p.offsetLeft;
		y-= p.offsetTop;
		
		p = p.offsetParent;
	} while(p!=null);
	
	return [x, y];
}

Page loaded in 0.01 second(s).