-
-
Save ngxson/8320c0903356319b0bee45a7362201a4 to your computer and use it in GitHub Desktop.
Basic Snake HTML and JavaScript Game - written in Functional programming style
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width"> | |
<title>Basic Snake HTML Game (Functional programming)</title> | |
<meta charset="UTF-8"> | |
<style> | |
html, body { | |
height: 100%; | |
margin: 0; | |
} | |
body { | |
background: black; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
} | |
.snake-demo { | |
text-align: center; | |
position: relative; | |
} | |
.snake-demo canvas { | |
max-width: 100vw; | |
border: 1px solid white; | |
margin: 0 auto; | |
} | |
.snake-demo button { | |
padding: 0.5em 1em; | |
margin-bottom: 1em; | |
} | |
.snake-demo .start-game { | |
position: absolute; | |
top: 5em; | |
left: 50%; | |
transform: translate(-50%, 0%); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="snake-demo"> | |
<div> | |
<button onclick="startGame()" class="btn start-game" id="start-game">Start game</button> | |
<canvas width="400" height="400" id="game"></canvas> | |
</div> | |
<!-- For touch screen --> | |
<br/> | |
<button onclick="handleKeyPress({ which: 38 })" class="btn">Up</button><br/> | |
<button onclick="handleKeyPress({ which: 37 })" class="btn">Left</button> | |
| |
<button onclick="handleKeyPress({ which: 39 })" class="btn">Right</button><br/> | |
<button onclick="handleKeyPress({ which: 40 })" class="btn">Down</button> | |
</div> | |
<script> | |
const canvas = document.getElementById('game'); | |
const context = canvas.getContext('2d'); | |
const timePerFrame = 250; | |
const pixelPerCell = 16; | |
const grid = 25; | |
var snakeNextDirection; // read from keyboard | |
var snakeCells; | |
var snakeDirection; | |
var food; | |
var timerId; | |
function startGame() { | |
snakeNextDirection = 'r'; | |
document.getElementById('start-game').style.display = 'none'; // hide start game button | |
timerId = setInterval(gameLoop, timePerFrame); | |
} | |
// game loop | |
function gameLoop() { | |
snakeDirection = snakeNextDirection; | |
if (!snakeCells) { | |
// if game is not initialized, create a new game | |
var game = getNewGame(); | |
snakeCells = game.snakeCells; | |
snakeDirection = game.snakeDirection; | |
food = game.food; | |
render(snakeCells, food); // render initial frame | |
return; | |
} | |
const currentHead = getCurrentHead(snakeCells); | |
const newHead = getNewHead(currentHead, snakeDirection); | |
if (isEatingSelf(snakeCells, newHead)) { | |
alert(`Game over! Score: ${getScore(snakeCells)}`); | |
clearInterval(timerId); // stop game loop | |
snakeCells = null; // reset game | |
document.getElementById('start-game').style.display = 'block'; // show start game button | |
return; | |
} | |
if (isEatingFood(food, newHead)) { | |
snakeCells = getNewSnake(snakeCells, newHead, true); | |
const emptyCells = getEmptyCells(snakeCells); | |
food = getNewFood(emptyCells); | |
} else { | |
snakeCells = getNewSnake(snakeCells, newHead, false); | |
} | |
render(snakeCells, food); | |
}; | |
function getNewGame() { | |
const snakeCells = [ | |
{ x: 7, y: 8 }, | |
{ x: 8, y: 8 }, | |
{ x: 9, y: 8 }, | |
]; | |
const snakeDirection = 'r'; | |
const food = { x: 12, y: 12 }; | |
return { snakeCells, snakeDirection, food }; | |
} | |
function getScore(snakeCells) { | |
return snakeCells.length - 3; | |
} | |
function wrapScreen(cell) { | |
// if x, y are out of screen, we wrap it back | |
return { | |
x: cell.x < 0 ? grid-1 : (cell.x % grid), | |
y: cell.y < 0 ? grid-1 : (cell.y % grid), | |
}; | |
} | |
function getCurrentHead(snakeCells) { | |
return snakeCells[snakeCells.length - 1]; | |
} | |
function getNewHead(currentHead, snakeDirection) { | |
if (snakeDirection == 'r') { // right | |
return wrapScreen({ x: currentHead.x + 1, y: currentHead.y }); | |
} else if (snakeDirection == 'l') { // left | |
return wrapScreen({ x: currentHead.x - 1, y: currentHead.y }); | |
} else if (snakeDirection == 'u') { // up | |
return wrapScreen({ x: currentHead.x, y: currentHead.y - 1 }); | |
} else if (snakeDirection == 'd') { // down | |
return wrapScreen({ x: currentHead.x, y: currentHead.y + 1 }); | |
} | |
} | |
function isEatingFood(food, newHead) { | |
return newHead.x == food.x && newHead.y == food.y; | |
} | |
function isEatingSelf(snakeCells, newHead) { | |
// i = 1 to skip oldest cell | |
for (let i = 1; i < snakeCells.length; i++) { | |
if (newHead.x == snakeCells[i].x && newHead.y == snakeCells[i].y) { | |
return true; | |
} | |
} | |
return false; | |
} | |
function getNewSnake(snakeCells, newHead, isLonger) { | |
const newSnake = [...snakeCells]; | |
if (!isLonger) { | |
// remove oldest cell if snake is not longer than before | |
newSnake.shift(); | |
} | |
newSnake.push(newHead); // add next cell | |
return newSnake; | |
} | |
function getEmptyCells(snakeCells) { | |
const cells = {}; | |
// construct list of all cells possible | |
for (let y = 0; y < grid; y++) { | |
for (let x = 0; x < grid; x++) { | |
cells[`${x}_${y}`] = { x, y }; | |
} | |
} | |
// remove cells occupied by snake | |
for (const cell of snakeCells) { | |
delete cells[`${cell.x}_${cell.y}`]; | |
} | |
return Object.values(cells); | |
} | |
function getNewFood(emptyCells) { | |
// take one random cell | |
return emptyCells[Math.floor(Math.random()*emptyCells.length)]; | |
} | |
function render(snakeCells, food) { | |
context.clearRect(0, 0, canvas.width, canvas.height); | |
// draw food | |
context.fillStyle = 'red'; | |
context.fillRect(food.x*pixelPerCell, food.y*pixelPerCell, pixelPerCell - 1, pixelPerCell - 1); | |
// draw snake cells | |
for (const cell of snakeCells) { | |
context.fillStyle = 'green'; | |
context.fillRect(cell.x*pixelPerCell, cell.y*pixelPerCell, pixelPerCell - 1, pixelPerCell - 1); | |
} | |
} | |
function handleKeyPress(e) { | |
// left arrow key | |
if (e.which === 37 && snakeDirection != 'r') { | |
snakeNextDirection = 'l'; | |
} | |
// up arrow key | |
else if (e.which === 38 && snakeDirection != 'd') { | |
snakeNextDirection = 'u'; | |
} | |
// right arrow key | |
else if (e.which === 39 && snakeDirection != 'l') { | |
snakeNextDirection = 'r'; | |
} | |
// down arrow key | |
else if (e.which === 40 && snakeDirection != 'u') { | |
snakeNextDirection = 'd'; | |
} | |
} | |
// listen to keyboard events to move the snake | |
document.addEventListener('keydown', handleKeyPress); | |
// on screen buttons for touch screen | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment