Skip to content

Instantly share code, notes, and snippets.

@ngxson
Forked from straker/README.md
Last active July 22, 2024 20:32
Show Gist options
  • Save ngxson/8320c0903356319b0bee45a7362201a4 to your computer and use it in GitHub Desktop.
Save ngxson/8320c0903356319b0bee45a7362201a4 to your computer and use it in GitHub Desktop.
Basic Snake HTML and JavaScript Game - written in Functional programming style
<!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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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