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]; }