Updated snake with a level editor.

This commit is contained in:
Jonathan Bernard 2014-12-09 00:00:25 -06:00
parent 59981daf60
commit 379526b65d
3 changed files with 242 additions and 76 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sw?

View File

@ -2,36 +2,72 @@
<html> <html>
<head> <head>
<script src="snake.js" type="application/javascript"></script> <script src="snake.js" type="application/javascript"></script>
<style type="text/css">
canvas { border: solid thin gray; }
section {
display: inline-block;
margin: 0.5rem;
position: relative; }
label {
display: inline-block;
width: 12rem; }
input[type=button] {
position: absolute;
right: 0; }
#editorControls { display: none; }
</style>
</head> </head>
<body> <body>
<h1>Snake</h1> <h1>Snake</h1>
<section id=controls>
<label>Rows</label> <label>Rows</label>
<input id=rows type=text placeholder="Rows" value=20> <input id=rows type=text placeholder="Rows" value=20><br>
<label>Columns</label> <label>Columns</label>
<input id=cols type=text placeholder="Columns" value=20> <input id=cols type=text placeholder="Columns" value=40><br>
<label>Speed (higher = faster)</label> <label>Speed (higher = faster)</label>
<input id=fps type=text placeholder="Speed (higher = faster)" value=10> <input id=fps type=text placeholder="Speed (higher = faster)" value=10><br>
<input name=editor type=radio value="Game" checked>Game</input>
<input id=useEditor name=editor type=radio value="Editor">Editor</input>
<input id=reset type=button value="Apply"> <input id=reset type=button value="Apply">
</section>
<section id=gameData>
<label> Custom Level:</label><br>
<textarea class=boardData></textarea>
</section>
<section id=editorControls>
<textarea class=boardData></textarea>
<input type=button value=Reload>
</section>
<section id=help>
Arrow keys change direction.<br>
Pause key pauses the game.<br>
Enter restarts the game.<br>
</section>
<br/> <br>
<canvas id=gameCanvas width=600 height=600></canvas> <canvas id=gameCanvas width=800 height=400 tabIndex=100></canvas>
<script type="application/javascript"> <script type="application/javascript">
var canvas = document.getElementById("gameCanvas"); var canvas = document.getElementById("gameCanvas");
var resetButton = document.getElementById("reset"); var resetButton = document.getElementById("reset");
var customLevelData = document.querySelector("#gameData textarea");
Snake.initialize({ canvas: canvas, Snake.initialize({ rows: parseInt(document.getElementById("rows").value),
keyContext: window,
rows: parseInt(document.getElementById("rows").value),
cols: parseInt(document.getElementById("cols").value), cols: parseInt(document.getElementById("cols").value),
fps: parseInt(document.getElementById("fps").value)}); fps: parseInt(document.getElementById("fps").value)});
resetButton.onclick = function() { resetButton.onclick = function() {
Snake.initialize({ canvas: canvas, if (customLevelData.value.trim().length > 0) {
keyContext: window, Snake.initialize(JSON.parse(customLevelData.value)); }
else { Snake.initialize({
editor: document.getElementById("useEditor").checked,
rows: parseInt(document.getElementById("rows").value), rows: parseInt(document.getElementById("rows").value),
cols: parseInt(document.getElementById("cols").value), cols: parseInt(document.getElementById("cols").value),
fps: parseInt(document.getElementById("fps").value)}); }; fps: parseInt(document.getElementById("fps").value)}); } };
</script> </script>
</body> </body>
</html> </html>

View File

@ -2,49 +2,85 @@
var S = window.Snake = {}; var S = window.Snake = {};
var SPACE = 0; var WALL = 1; var SNAKE = 2; var FOOD = 3;
var ROWS = S.ROWS = 50; var ROWS = S.ROWS = 50;
var COLS = S.COLS = 50; var COLS = S.COLS = 50;
var board; var board;
var startBoard;
var tileWidth, tileHeight;
var body = new Array(); var body = new Array();
var headCur = coord2idx(Math.floor((ROWS - 1) / 2), Math.floor((COLS - 1) / 2)); var headCur;
var direction = 1; var direction = 1;
var g = S.graphicsContext = null;
var canvas = S.canvas = null;
var cellWidth, cellHeight;
var foodEaten = false; var foodEaten = false;
var g;
var canvas;
var editor = false;
var editorControls;
var editorDataTextarea;
var cursorIdx;
var cursorBlock;
var dead = true; var dead = true;
var pause = false;
var skipTicks; var skipTicks;
var nextGameTick; var nextGameTick;
var cmdQueue = new Array(); var cmdQueue = new Array();
var handers = new Array();
function coord2idx(row, col) { return (COLS * row) + col; } function coord2idx(row, col) { return (COLS * row) + col; }
function idx2row(idx) { return Math.floor(idx/COLS); } function idx2row(idx) { return Math.floor(idx/COLS); }
function idx2col(idx) { return idx % COLS; } function idx2col(idx) { return idx % COLS; }
function initialize(options) { //givenCanvas, keyContext, fps) { function initialize(options) {
if (!options.canvas) { // Store our own copy of the canvas and get a 2D graphics context.
alert("Missing the canvas element."); if (options.canvas) canvas = options.canvas;
return false } else canvas = document.getElementsByTagName("canvas")[0];
canvas = options.canvas;
g = S.graphicsContext = canvas.getContext("2d"); 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.rows) S.ROWS = ROWS = options.rows;
if (options.cols) S.COLS = COLS = options.cols; if (options.cols) S.COLS = COLS = options.cols;
board = new Array(ROWS * 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; } }
cellWidth = canvas.width / COLS; // Figure out how big each game tile is.
cellHeight = canvas.height / ROWS; tileWidth = canvas.width / COLS;
tileHeight = canvas.height / ROWS;
if (options.keyContext) options.keyContext.onkeydown = handleKey; // Mark the player as dead. This is primarily for the case where a
else canvas.onkeydown = handleKey; // 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.fillStyle = "white";
g.fillRect(0, 0, canvas.width, canvas.height); g.fillRect(0, 0, canvas.width, canvas.height);
if (!editor) {
canvas.addEventListener('keydown', handleGameKey);
g.font = "24px sans-serif"; g.font = "24px sans-serif";
g.fillStyle = "black"; g.fillStyle = "black";
g.fillText("Press Enter to begin.", 50, Math.floor(canvas.height/2), canvas.width - 100); g.fillText("Press Enter to begin.", 50, Math.floor(canvas.height/2), canvas.width - 100);
@ -52,38 +88,68 @@
if (options.fps) skipTicks = Math.ceil(1000 / options.fps); if (options.fps) skipTicks = Math.ceil(1000 / options.fps);
else skipTicks = 150; } else skipTicks = 150; }
function startGame(canvas, keyContext) { 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 (body.length > 0) body.pop();
while (cmdQueue.length > 0) cmdQueue.pop(); while (cmdQueue.length > 0) cmdQueue.pop();
headCur = coord2idx(Math.floor((ROWS - 1) / 2), Math.floor((COLS - 1) / 2)); headCur = coord2idx(Math.floor((ROWS - 1) / 2), Math.floor((COLS - 1) / 2));
direction = 1; direction = 1;
dead = false; dead = pause = false;
nextGameTick = (new Date).getTime() + skipTicks; nextGameTick = (new Date).getTime() + skipTicks;
body.push(headCur); body.push(headCur);
// Wipe the board and draw the walls. // Copy over a fresh board.
var i; board = new Array(startBoard.length);
for (i = 0; i < board.length; i++ ) { for (var i = 0; i < board.length; i++) board[i] = startBoard[i];
if (Math.floor(i / COLS) == 0 || (i % COLS) == 0) board[i] = 'W';
else if (Math.floor(i / COLS) == (ROWS - 1) ||
(i % COLS) == (COLS - 1)) board[i] = 'W';
else board[i] = ''; }
// Write the initial body segment onto the board.
board[headCur] = 'S';
// Create a new food item and add it to the board. // Create a new food item and add it to the board.
board[newFoodLocation()] = 'F'; board[newFoodLocation()] = FOOD;
// Write the initial body segment onto the board.
board[headCur] = SNAKE;
// Draw the board // Draw the board
for (i = 0; i < board.length; i++) { paintIdx(i); } paintBoard();
gameLoop(); } 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() { function gameLoop() {
if (dead) return // break the callback loop // Break the game loop if the player died or paused.
if (dead || pause) return;
if ((new Date).getTime() > nextGameTick) { if ((new Date).getTime() > nextGameTick) {
updateAndDraw(); updateAndDraw();
@ -114,35 +180,35 @@
// 3. First check to see if we've eaten food (since it will affect // 3. First check to see if we've eaten food (since it will affect
// collision detection for the body). This is also where growth // collision detection for the body). This is also where growth
// happens via not moving the tail. // happens via not moving the tail.
if (board[headCur] == 'F') foodEaten = true; if (board[headCur] == FOOD) foodEaten = true;
// 4. Remove the tail. // 4. Remove the tail.
if (!foodEaten) { if (!foodEaten) {
var tailIdx = body.shift(); var tailIdx = body.shift();
// Value on the board doesn't matter, just not W,S, or F // Value on the board doesn't matter, just not W,S, or F
board[tailIdx] = ''; board[tailIdx] = SPACE
paintIdx(tailIdx); } paintIdx(tailIdx); }
// 5. Detect wall and snake collisions // 5. Detect wall and snake collisions
if (board[headCur] == 'W' || board[headCur] == 'S') { if (board[headCur] == WALL || board[headCur] == SNAKE) {
die(); return } die(); return }
// 6. Move the head // 6. Move the head
body.push(headCur); body.push(headCur);
board[headCur] = 'S'; board[headCur] = SNAKE;
paintIdx(headCur); paintIdx(headCur);
// 7. Create more food if needed. // 7. Create more food if needed.
if (foodEaten) { if (foodEaten) {
var foodLoc = newFoodLocation(); var foodLoc = newFoodLocation();
board[foodLoc] = 'F'; board[foodLoc] = FOOD;
paintIdx(foodLoc); } } paintIdx(foodLoc); } }
function newFoodLocation() { function newFoodLocation() {
var emptySpaces = new Array(); var emptySpaces = new Array();
for (var i = 0; i < board.length; i++) for (var i = 0; i < board.length; i++)
if (board[i] != 'W' && board[i] != 'S') if (board[i] != WALL && board[i] != SNAKE)
emptySpaces.push(i); emptySpaces.push(i);
return emptySpaces[Math.floor(Math.random() * emptySpaces.length)] } return emptySpaces[Math.floor(Math.random() * emptySpaces.length)] }
@ -157,42 +223,105 @@
g.fillStyle = "red"; g.fillStyle = "red";
g.fillText("Press Enter to retry.", 50, Math.floor(canvas.height/2), canvas.width - 100); } g.fillText("Press Enter to retry.", 50, Math.floor(canvas.height/2), canvas.width - 100); }
function handleKey(ke) { function handleGameKey(ke) {
var key = ke.keyCode ? ke.keyCode : ke.which; var key = ke.keyCode ? ke.keyCode : ke.which;
var dontCare = false;
// Enter, start a new game. // Enter, start a new game.
if (key == 13) startGame(); 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 // Queue directions in the command queue
else if ((key > 36) && (key < 41)) cmdQueue.push(key); } 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) { function paintCoord(row, col) {
var idx = coord2idx(row, col); var idx = coord2idx(row, col);
if (board[idx] == 'W') g.fillStyle = "black"; if (board[idx] == WALL) g.fillStyle = "black";
else if (board[idx] == 'F') g.fillStyle = "yellow"; else if (board[idx] == FOOD) g.fillStyle = "green";
else if (board[idx] == 'S') g.fillStyle = "blue"; else if (board[idx] == SNAKE) g.fillStyle = "blue";
else g.fillStyle = "white"; else g.fillStyle = "white";
g.fillRect(col, row, cellWidth, cellHeight); } g.fillRect(col, row, tileWidth, tileHeight); }
function paintIdxColor(idx, color) { function paintIdxColor(idx, color) {
g.fillStyle = color; g.fillStyle = color;
g.fillRect( g.fillRect(
(idx % COLS) * cellWidth, (idx % COLS) * tileWidth,
Math.floor(idx / COLS) * cellHeight, Math.floor(idx / COLS) * tileHeight,
cellWidth, cellHeight); } tileWidth, tileHeight); }
function paintIdx(idx) { function paintIdx(idx) {
if (board[idx] == 'W') g.fillStyle = "black"; if (board[idx] == WALL) g.fillStyle = "black";
else if (board[idx] == 'F') g.fillStyle = "yellow"; else if (board[idx] == FOOD) g.fillStyle = "green";
else if (board[idx] == 'S') g.fillStyle = "blue"; else if (board[idx] == SNAKE) g.fillStyle = "blue";
else g.fillStyle = "white"; else g.fillStyle = "white";
g.fillRect( g.fillRect(
(idx % COLS) * cellWidth, (idx % COLS) * tileWidth,
Math.floor(idx / COLS) * cellHeight, Math.floor(idx / COLS) * tileHeight,
cellWidth, cellHeight); } tileWidth, tileHeight); }
S.initialize = initialize; S.initialize = initialize;