(function() { var S = window.Snake = {}; var SPACE = 0; var WALL = 1; var SNAKE = 2; var FOOD = 3; var ROWS = S.ROWS = 50; var COLS = S.COLS = 50; var board; var startBoard; var tileWidth, tileHeight; var body = new Array(); var headCur; var direction = 1; var foodEaten = false; var g; var canvas; var editor = false; var editorControls; var editorDataTextarea; var cursorIdx; var cursorBlock; var dead = true; var pause = false; var skipTicks; var nextGameTick; var cmdQueue = new Array(); var handers = new Array(); function coord2idx(row, col) { return (COLS * row) + col; } function idx2row(idx) { return Math.floor(idx/COLS); } function idx2col(idx) { return idx % COLS; } function initialize(options) { // Store our own copy of the canvas and get a 2D graphics context. if (options.canvas) canvas = options.canvas; else canvas = document.getElementsByTagName("canvas")[0]; g = S.graphicsContext = canvas.getContext("2d"); // Remove any existing handlers canvas.removeEventListener('keydown', handleGameKey); canvas.removeEventListener('keydown', handleEditorKey); canvas.removeEventListener('click', handleEditorClick); // Read in the board dimensions. if (options.rows) S.ROWS = ROWS = options.rows; if (options.cols) S.COLS = COLS = options.cols; if (options.board) startBoard = options.board; else { // Wipe the board and draw the walls. startBoard = new Array(ROWS * COLS); var i; for (i = 0; i < startBoard.length; i++ ) { if (Math.floor(i / COLS) == 0 || (i % COLS) == 0) startBoard[i] = WALL else if (Math.floor(i / COLS) == (ROWS - 1) || (i % COLS) == (COLS - 1)) startBoard[i] = WALL else startBoard[i] = SPACE; } } // Figure out how big each game tile is. tileWidth = canvas.width / COLS; tileHeight = canvas.height / ROWS; // Mark the player as dead. This is primarily for the case where a // player re-initializes the board during a game in progress. It // causes any scheduled logic to quit without affecting out game state. dead = true; // Check to see if they want to use the editor or the game. editor = Boolean(options.editor); g.fillStyle = "white"; g.fillRect(0, 0, canvas.width, canvas.height); if (!editor) { canvas.addEventListener('keydown', handleGameKey); g.font = "24px sans-serif"; g.fillStyle = "black"; g.fillText("Press Enter to begin.", 50, Math.floor(canvas.height/2), canvas.width - 100); if (options.fps) skipTicks = Math.ceil(1000 / options.fps); else skipTicks = 150; } else { canvas.addEventListener('keydown', handleEditorKey); canvas.addEventListener('click', handleEditorClick); editorControls = document.getElementById("editorControls") editorDataTextarea = document.querySelector("#editorControls textarea"); startEditor(); } canvas.focus(); } function startGame() { while (body.length > 0) body.pop(); while (cmdQueue.length > 0) cmdQueue.pop(); headCur = coord2idx(Math.floor((ROWS - 1) / 2), Math.floor((COLS - 1) / 2)); direction = 1; dead = pause = false; nextGameTick = (new Date).getTime() + skipTicks; body.push(headCur); // Copy over a fresh board. board = new Array(startBoard.length); for (var i = 0; i < board.length; i++) board[i] = startBoard[i]; // Create a new food item and add it to the board. board[newFoodLocation()] = FOOD; // Write the initial body segment onto the board. board[headCur] = SNAKE; // Draw the board paintBoard(); gameLoop(); } function startEditor() { board = new Array(ROWS * COLS); cursorBlock = SPACE; cursorIdx = headCur = coord2idx( Math.floor((ROWS - 1) / 2), Math.floor((COLS - 1) / 2)); // Wipe the board and draw the walls. var i; for (i = 0; i < board.length; i++ ) { if (Math.floor(i / COLS) == 0 || (i % COLS) == 0) board[i] = WALL else if (Math.floor(i / COLS) == (ROWS - 1) || (i % COLS) == (COLS - 1)) board[i] = WALL else board[i] = SPACE; } board[headCur] = SNAKE; document.getElementById("editorControls").style.display = "inline-block"; paintBoard(); } function gameLoop() { // Break the game loop if the player died or paused. if (dead || pause) return; if ((new Date).getTime() > nextGameTick) { updateAndDraw(); nextGameTick += skipTicks; } requestAnimationFrame(gameLoop); } // For this game I want the game logic and the refresh rate to be tied // together. The snake moves on step per animation frame. We do limit the // animation frame to 1fps. This works because there are no other elements // being animated other than the snake. function updateAndDraw() { if (!g) return false; foodEaten = false; // 1. Read up to one command from the user. if (cmdQueue.length > 0) { var cmd = cmdQueue.shift(); if (cmd == 37) direction = -1; // Left else if (cmd == 38) direction = -COLS; // Up else if (cmd == 39) direction = 1; // Right else if (cmd == 40) direction = COLS; /* Down */ } // 2. Find the head's next spot. headCur = headCur + direction; // 3. First check to see if we've eaten food (since it will affect // collision detection for the body). This is also where growth // happens via not moving the tail. if (board[headCur] == FOOD) foodEaten = true; // 4. Remove the tail. if (!foodEaten) { var tailIdx = body.shift(); // Value on the board doesn't matter, just not W,S, or F board[tailIdx] = SPACE paintIdx(tailIdx); } // 5. Detect wall and snake collisions if (board[headCur] == WALL || board[headCur] == SNAKE) { die(); return } // 6. Move the head body.push(headCur); board[headCur] = SNAKE; paintIdx(headCur); // 7. Create more food if needed. if (foodEaten) { var foodLoc = newFoodLocation(); board[foodLoc] = FOOD; paintIdx(foodLoc); } } function newFoodLocation() { var emptySpaces = new Array(); for (var i = 0; i < board.length; i++) if (board[i] != WALL && board[i] != SNAKE) emptySpaces.push(i); return emptySpaces[Math.floor(Math.random() * emptySpaces.length)] } function die() { paintIdxColor(headCur, "red"); dead = true; //g.fillStyle = "white"; //g.fillRect(0, 0, canvas.width, canvas.height); g.font = "24px sans-serif"; g.fillStyle = "red"; g.fillText("Press Enter to retry.", 50, Math.floor(canvas.height/2), canvas.width - 100); } function handleGameKey(ke) { var key = ke.keyCode ? ke.keyCode : ke.which; var dontCare = false; // Enter, start a new game. if (key == 13) { dead = true; requestAnimationFrame(startGame); } // Pause, pause the game. else if (key == 19) { if (pause == true) { pause = false; nextGameTick = (new Date).getTime(); gameLoop(); } else pause = true; } // Queue directions in the command queue else if ((key > 36) && (key < 41)) cmdQueue.push(key); else dontCare = true; if (!dontCare) ke.preventDefault(); return false; } function handleEditorKey(ke) { var key = ke.keyCode ? ke.keyCode : ke.which; var dontCare = false; switch (key) { // Enter: place a tile. case 13: board[cursorIdx] = cursorBlock; break; case 37: cursorIdx -= 1; break; // Left case 38: cursorIdx -= COLS; break; // Up case 39: cursorIdx += 1; break; // Right case 40: cursorIdx += COLS; break; // Down case 49: case 97: cursorBlock = SPACE; break; // 1 case 50: case 98: cursorBlock = WALL; break; // 2 case 51: case 99: cursorBlock = SNAKE; break; // 3 case 52: case 100: cursorBlock = FOOD; break; // 4 default: dontCare = true; } emitEditorLevelData(); paintBoard(); g.strokeStyle = "solid 4px gray"; g.strokeRect( (cursorIdx % COLS) * tileWidth, Math.floor(cursorIdx / COLS) * tileHeight, tileWidth, tileHeight); if (!dontCare) ke.preventDefault(); } function handleEditorClick(ke) { } function emitEditorLevelData() { editorDataTextarea.value = JSON.stringify({ board: board, rows: ROWS, cols: COLS, fps: Math.ceil(1000 / skipTicks)}); } function loadEditorLevelData() { } function paintBoard() { for (i = 0; i < board.length; i++) { paintIdx(i); } } function paintCoord(row, col) { var idx = coord2idx(row, col); if (board[idx] == WALL) g.fillStyle = "black"; else if (board[idx] == FOOD) g.fillStyle = "green"; else if (board[idx] == SNAKE) g.fillStyle = "blue"; else g.fillStyle = "white"; g.fillRect(col, row, tileWidth, tileHeight); } function paintIdxColor(idx, color) { g.fillStyle = color; g.fillRect( (idx % COLS) * tileWidth, Math.floor(idx / COLS) * tileHeight, tileWidth, tileHeight); } function paintIdx(idx) { if (board[idx] == WALL) g.fillStyle = "black"; else if (board[idx] == FOOD) g.fillStyle = "green"; else if (board[idx] == SNAKE) g.fillStyle = "blue"; else g.fillStyle = "white"; g.fillRect( (idx % COLS) * tileWidth, Math.floor(idx / COLS) * tileHeight, tileWidth, tileHeight); } S.initialize = initialize; // Exports for debugging S.board = board; S.body = body; S.updateAndDraw = updateAndDraw; })();