Updated snake with a level editor.
This commit is contained in:
parent
59981daf60
commit
379526b65d
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sw?
|
@ -2,36 +2,72 @@
|
||||
<html>
|
||||
<head>
|
||||
<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>
|
||||
<body>
|
||||
<h1>Snake</h1>
|
||||
<label>Rows</label>
|
||||
<input id=rows type=text placeholder="Rows" value=20>
|
||||
<label>Columns</label>
|
||||
<input id=cols type=text placeholder="Columns" value=20>
|
||||
<label>Speed (higher = faster)</label>
|
||||
<input id=fps type=text placeholder="Speed (higher = faster)" value=10>
|
||||
<input id=reset type=button value="Apply">
|
||||
<section id=controls>
|
||||
<label>Rows</label>
|
||||
<input id=rows type=text placeholder="Rows" value=20><br>
|
||||
<label>Columns</label>
|
||||
<input id=cols type=text placeholder="Columns" value=40><br>
|
||||
<label>Speed (higher = faster)</label>
|
||||
<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">
|
||||
</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/>
|
||||
<canvas id=gameCanvas width=600 height=600></canvas>
|
||||
<br>
|
||||
<canvas id=gameCanvas width=800 height=400 tabIndex=100></canvas>
|
||||
|
||||
<script type="application/javascript">
|
||||
var canvas = document.getElementById("gameCanvas");
|
||||
var resetButton = document.getElementById("reset");
|
||||
var customLevelData = document.querySelector("#gameData textarea");
|
||||
|
||||
Snake.initialize({ canvas: canvas,
|
||||
keyContext: window,
|
||||
rows: parseInt(document.getElementById("rows").value),
|
||||
Snake.initialize({ rows: parseInt(document.getElementById("rows").value),
|
||||
cols: parseInt(document.getElementById("cols").value),
|
||||
fps: parseInt(document.getElementById("fps").value)});
|
||||
|
||||
resetButton.onclick = function() {
|
||||
Snake.initialize({ canvas: canvas,
|
||||
keyContext: window,
|
||||
rows: parseInt(document.getElementById("rows").value),
|
||||
cols: parseInt(document.getElementById("cols").value),
|
||||
fps: parseInt(document.getElementById("fps").value)}); };
|
||||
if (customLevelData.value.trim().length > 0) {
|
||||
Snake.initialize(JSON.parse(customLevelData.value)); }
|
||||
else { Snake.initialize({
|
||||
editor: document.getElementById("useEditor").checked,
|
||||
rows: parseInt(document.getElementById("rows").value),
|
||||
cols: parseInt(document.getElementById("cols").value),
|
||||
fps: parseInt(document.getElementById("fps").value)}); } };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
243
snake/snake.js
243
snake/snake.js
@ -2,88 +2,154 @@
|
||||
|
||||
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 = coord2idx(Math.floor((ROWS - 1) / 2), Math.floor((COLS - 1) / 2));
|
||||
var headCur;
|
||||
var direction = 1;
|
||||
var g = S.graphicsContext = null;
|
||||
var canvas = S.canvas = null;
|
||||
var cellWidth, cellHeight;
|
||||
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) { //givenCanvas, keyContext, fps) {
|
||||
function initialize(options) {
|
||||
|
||||
if (!options.canvas) {
|
||||
alert("Missing the canvas element.");
|
||||
return false }
|
||||
|
||||
canvas = options.canvas;
|
||||
// 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;
|
||||
|
||||
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;
|
||||
cellHeight = canvas.height / ROWS;
|
||||
// Figure out how big each game tile is.
|
||||
tileWidth = canvas.width / COLS;
|
||||
tileHeight = canvas.height / ROWS;
|
||||
|
||||
if (options.keyContext) options.keyContext.onkeydown = handleKey;
|
||||
else canvas.onkeydown = handleKey;
|
||||
// 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);
|
||||
|
||||
g.font = "24px sans-serif";
|
||||
g.fillStyle = "black";
|
||||
g.fillText("Press Enter to begin.", 50, Math.floor(canvas.height/2), canvas.width - 100);
|
||||
if (!editor) {
|
||||
canvas.addEventListener('keydown', handleGameKey);
|
||||
|
||||
if (options.fps) skipTicks = Math.ceil(1000 / options.fps);
|
||||
else skipTicks = 150; }
|
||||
g.font = "24px sans-serif";
|
||||
g.fillStyle = "black";
|
||||
g.fillText("Press Enter to begin.", 50, Math.floor(canvas.height/2), canvas.width - 100);
|
||||
|
||||
function startGame(canvas, keyContext) {
|
||||
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 = false;
|
||||
dead = pause = false;
|
||||
nextGameTick = (new Date).getTime() + skipTicks;
|
||||
|
||||
body.push(headCur);
|
||||
|
||||
// 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] = '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';
|
||||
// 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()] = 'F';
|
||||
board[newFoodLocation()] = FOOD;
|
||||
|
||||
// Write the initial body segment onto the board.
|
||||
board[headCur] = SNAKE;
|
||||
|
||||
// Draw the board
|
||||
for (i = 0; i < board.length; i++) { paintIdx(i); }
|
||||
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() {
|
||||
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) {
|
||||
updateAndDraw();
|
||||
@ -114,35 +180,35 @@
|
||||
// 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] == 'F') foodEaten = true;
|
||||
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] = '';
|
||||
board[tailIdx] = SPACE
|
||||
paintIdx(tailIdx); }
|
||||
|
||||
|
||||
// 5. Detect wall and snake collisions
|
||||
if (board[headCur] == 'W' || board[headCur] == 'S') {
|
||||
if (board[headCur] == WALL || board[headCur] == SNAKE) {
|
||||
die(); return }
|
||||
|
||||
// 6. Move the head
|
||||
body.push(headCur);
|
||||
board[headCur] = 'S';
|
||||
board[headCur] = SNAKE;
|
||||
paintIdx(headCur);
|
||||
|
||||
// 7. Create more food if needed.
|
||||
if (foodEaten) {
|
||||
var foodLoc = newFoodLocation();
|
||||
board[foodLoc] = 'F';
|
||||
board[foodLoc] = FOOD;
|
||||
paintIdx(foodLoc); } }
|
||||
|
||||
function newFoodLocation() {
|
||||
var emptySpaces = new Array();
|
||||
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);
|
||||
|
||||
return emptySpaces[Math.floor(Math.random() * emptySpaces.length)] }
|
||||
@ -157,42 +223,105 @@
|
||||
g.fillStyle = "red";
|
||||
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 dontCare = false;
|
||||
|
||||
// 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
|
||||
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) {
|
||||
var idx = coord2idx(row, col);
|
||||
if (board[idx] == 'W') g.fillStyle = "black";
|
||||
else if (board[idx] == 'F') g.fillStyle = "yellow";
|
||||
else if (board[idx] == 'S') g.fillStyle = "blue";
|
||||
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, cellWidth, cellHeight); }
|
||||
g.fillRect(col, row, tileWidth, tileHeight); }
|
||||
|
||||
function paintIdxColor(idx, color) {
|
||||
g.fillStyle = color;
|
||||
|
||||
g.fillRect(
|
||||
(idx % COLS) * cellWidth,
|
||||
Math.floor(idx / COLS) * cellHeight,
|
||||
cellWidth, cellHeight); }
|
||||
(idx % COLS) * tileWidth,
|
||||
Math.floor(idx / COLS) * tileHeight,
|
||||
tileWidth, tileHeight); }
|
||||
|
||||
function paintIdx(idx) {
|
||||
if (board[idx] == 'W') g.fillStyle = "black";
|
||||
else if (board[idx] == 'F') g.fillStyle = "yellow";
|
||||
else if (board[idx] == 'S') g.fillStyle = "blue";
|
||||
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) * cellWidth,
|
||||
Math.floor(idx / COLS) * cellHeight,
|
||||
cellWidth, cellHeight); }
|
||||
(idx % COLS) * tileWidth,
|
||||
Math.floor(idx / COLS) * tileHeight,
|
||||
tileWidth, tileHeight); }
|
||||
|
||||
S.initialize = initialize;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user