Snake Game

Javascript source code

var ctx = null;

var gameTime = 0, lastFrameTime = 0;
var currentSecond = 0, frameCount = 0, framesLastSecond = 0;
var showFramerate = false;

var offsetX = 0;
var offsetY = 0;

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

var gameState = {
	screen		: 'menu',
	
	dir		: 0,
	moveDelay	: 300,
	lastMove	: 0,
	
	snake		: [],
	newBlock	: null,
	
	mapW		: 14,
	mapH		: 14,
	tileW		: 20,
	tileH		: 20,
	
	score		: 0,
	newBest		: false,
	bestScore	: 0
};

function startGame()
{
	// Reset the game time
	gameTime	= 0;
	lastFrameTime	= 0;
	
	// Reset all of the gameState attributes to their
	// starting values (except those that need to persist
	// between games)
	gameState.snake.length	= 0;
	gameState.dir		= 0;
	gameState.score		= 0;
	gameState.lastMove	= 0;
	gameState.screen	= 'playing';
	gameState.newBest	= false;
	
	// Create the snake head at the centre of the map
	gameState.snake.push([
		Math.floor(gameState.mapW / 2),
		Math.floor(gameState.mapH / 2)
	]);
	
	// Place a random "food" block on the map
	placeNewBlock();
}

function placeNewBlock()
{
	do {
		// Choose a random coordinate somewhere on the map
		var x = Math.floor(Math.random() * gameState.mapW);
		var y = Math.floor(Math.random() * gameState.mapH);
		
		// Currently we don't think this block falls on the
		// snakes body
		var onSnake = false;
		
		// Loop through the snakes body segments
		for(var s in gameState.snake)
		{
			// If this is on the snakes body, change our
			// onSnake value to true
			if(gameState.snake[s][0]==x &&
				gameState.snake[s][1]==y)
			{
				onSnake = true;
				break;
			}
		}
		
		// If the coordinates are not on the snakes body,
		// place the newBlock here and exit out of the
		// loop and function
		if(!onSnake)
		{
			gameState.newBlock = [x, y];
			break;
		}
	
	// Keep trying until the block can be placed
	} while(true);
}

function updateGame()
{
	// Depending on the current screen being displayed, we
	// handle game updates differently
	
	if(gameState.screen=='menu')
	{
		if(mouseState.click!=null)
		{
			// If there's been a click on the screen and
			// it falls on the "New Game" line of text,
			// call the startGame function
			if(mouseState.click[1] >= 150 &&
				mouseState.click[1]<= 220)
			{
				startGame();
			}
		}
		
		// Clear the click state (we've done the processing
		// for it already)
		mouseState.click = null;
	}
	else if(gameState.screen=='playing')
	{
		// If enough time hasn't elapsed since the snake began
		// moving from the previous tile to the next, we don't
		// need to do anything yet, so leave the function
		if((gameTime - gameState.lastMove) < gameState.moveDelay)
		{
			return;
		}
		
		// Create a temporary variable storing the current
		// position of the snakes head, and also the current
		// direction of the snake
		var tmp = gameState.snake[0];
		var head = [tmp[0], tmp[1]];
		var dir = gameState.dir;

		// If the next position of the snakes head falls outside
		// of the maps bounds, we've lost; call gameOver
		if(dir==0 && head[1]==0) { gameOver(); }
		else if(dir==2 && head[1]==(gameState.mapH-1)) { gameOver(); }
		else if(dir==3 && head[0]==0) { gameOver(); }
		else if(dir==1 && head[0]==(gameState.mapW-1)) { gameOver(); }
		
		// Modify our head variable to the next position the
		// snakes head will occupy
		if(dir==0) { head[1]-= 1; }
		else if(dir==2) { head[1]+= 1; }
		else if(dir==1) { head[0]+= 1; }
		else if(dir==3) { head[0]-= 1; }
		
		// Loop through the snake body segments
		for(var s in gameState.snake)
		{
			// If this is the end of the snake, ignore it
			if(s==(gameState.snake.length-1)) { break; }
			
			// If the next position of the snakes head falls
			// on part of the snakes body, gameOver
			if(gameState.snake[s][0]==head[0] &&
				gameState.snake[s][1]==head[1])
			{
				gameOver();
				break;
			}
		}
		
		// If gameOver has been called, it will have changed
		// the current screen.  In this case, exit the function
		if(gameState.screen=='menu') { return; }
		
		// Put the new head position on the start of the snake
		// body array and update the lastMove to the current gameTime
		gameState.snake.unshift(head);
		gameState.lastMove = gameTime;
		
		// If the new head position is the same as the food position,
		// increase the score and place a random new food block.
		// Otherwise, remove the tail of the snake.
		if(gameState.newBlock[0]==head[0] &&
			gameState.newBlock[1]==head[1])
		{
			gameState.score+= 1;
			placeNewBlock();
		}
		else { gameState.snake.pop(); }
	}
}

function gameOver()
{
	gameState.screen = 'menu';
	if(gameState.score > gameState.bestScore)
	{
		// If a new best score has been achieved,
		// update the bestScore and newBest flag
		// accordingly
		gameState.bestScore	= gameState.score;
		gameState.newBest	= true;
	}
}

window.onload = function()
{
	// Create a reference to the Canvas 2D drawing context
	ctx = document.getElementById('game').getContext('2d');

	// Calculate the offsets needed to centre our map on the
	// Canvas
	offsetX = Math.floor((document.getElementById('game').width -
		(gameState.mapW * gameState.tileW)) / 2);
	offsetY = Math.floor((document.getElementById('game').height -
		(gameState.mapH * gameState.tileH)) / 2);
	
	document.getElementById('game').addEventListener('click', function(e) {
		// When the Canvas is clicked on, find the position
		// of the mouse click and record a click event in the
		// mouseState object
		var pos = realPos(e.pageX, e.pageY);
		mouseState.click = pos;
	});
	document.getElementById('game').addEventListener('mousemove',
	function(e) {
		// When the mouse is moved on the Canvas, find the true
		// cursor position and update the mouseState x,y accordingly
		var pos = realPos(e.pageX, e.pageY);
		mouseState.x = pos[0];
		mouseState.y = pos[1];
	});
	
	window.addEventListener('keydown', function(e) {
		// If an arrow key is pressed, update the direction (dir)
		// property accordingly.  If the F key is pressed, toggle
		// the visibility of the frame rate counter
		if(e.keyCode==38) { gameState.dir = 0; }
		else if(e.keyCode==39) { gameState.dir = 1; }
		else if(e.keyCode==40) { gameState.dir = 2; }
		else if(e.keyCode==37) { gameState.dir = 3; }
		else if(e.keyCode==70) { showFramerate = !showFramerate; }
	});
	
	// When the Canvas is ready to draw, call our drawGame method	
	requestAnimationFrame(drawGame);
};

function drawMenu()
{
	// Set the font for the main screen text
	ctx.textAlign	= "center";
	ctx.font	= "bold italic 20pt sans-serif";

	// Set the colour based on whether or not the moue cursor
	// is over the "New Game" text, then draw New Game
	ctx.fillStyle = ((mouseState.y>=150 && mouseState.y<=220) ?
		"#0000aa" : "#000000");
	
	ctx.fillText("New game", 150, 180);
	
	// Change the font and show the current best score
	ctx.font		= "italic 12pt sans-serif";
	ctx.fillText("Best score: " + gameState.bestScore, 150, 210);
	
	if(gameState.newBest)
	{
		// If the player achieved a new top score during their
		// last game, say so
		ctx.fillText("New top score!", 150, 240);
	}
	if(gameState.score>0)
	{
		// If the player has just finished a game and scored any
		// points, then show the last score
		ctx.fillText("Last score: " + gameState.score, 150, 260);
	}
}

function drawPlaying()
{
	// Set the stroke and fill colours
	ctx.strokeStyle = "#000000";
	ctx.fillStyle	= "#000000";
	
	// Draw the bouding area of the map
	ctx.strokeRect(offsetX, offsetY,
		(gameState.mapW * gameState.tileW),
		(gameState.mapH * gameState.tileH));
	
	for(var s in gameState.snake)
	{
		// Loop through the snake body segments and draw each of them
		ctx.fillRect(offsetX + (gameState.snake[s][0] * gameState.tileW),
			offsetY + (gameState.snake[s][1] * gameState.tileH),
			gameState.tileW, gameState.tileH);
	}
	
	// Set the font for the current score and show it
	ctx.font = "12pt sans-serif";
	ctx.textAlign = "right";
	ctx.fillText("Score: " + gameState.score, 290, 20);
	
	// Set the fill colour for the food, and draw it on the map
	ctx.fillStyle	= "#00cc00";
	ctx.fillRect(offsetX + (gameState.newBlock[0] * gameState.tileW),
		offsetY + (gameState.newBlock[1] * gameState.tileH),
		gameState.tileW, gameState.tileH);
}

function drawGame()
{
	if(ctx==null) { return; }
	
	// Frame & update related timing
	var currentFrameTime = Date.now();
	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);

	// Set font and show framerate (if toggled)
	if(showFramerate)
	{
		ctx.textAlign = "left";
		ctx.font = "10pt sans-serif";
		ctx.fillStyle = "#000000";
		ctx.fillText("Frames: " + framesLastSecond, 5, 15);
	}
	
	// Draw the current screen
	if(gameState.screen=='menu')		{ drawMenu(); }
	else if(gameState.screen=='playing')	{ drawPlaying(); }
	
	// 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.009 second(s).