<div class="container">
	<div class="score">		
		score: <span class="score_val">0</span>
	</div>
	<div class="canvas-wrapper">
		<canvas width="400" height="400"></canvas>
	</div>
	<div class="controls">
		<button class="btn-start">Start</button>
		<button class="btn-pause">Pause</button>
		<div class="range range-difficulty">
			<label for="difficulty">Speed</label>
			<div class="range_inputWrapper">
				<span class="range_inputSlider"></span>
				<div class="range_inputValue"></div>
				<input type="range" name="difficulty"
				   value="8" min="1" max="10" step="1" >
			</div>
		</div>
		
		<div class="range range-columns">
			<label for="columns">Cells</label>
			<div class="range_inputWrapper">
				<span class="range_inputSlider"></span>
				<div class="range_inputValue"></div>
				<input type="range" name="columns"
			     	value="20" min="10" max="100" step="10">
			</div>
		</div>
	</div>
</div>
* {
	box-sizing: border-box;
}

html, body {
	width: 100%;
	height: 100%;
	margin: 0;
	background: darken(#00796B, 5%);	
	font-family: 'Varela Round';
}

body {
	display: flex;
	justify-content: center;
	align-items: center;
}

.container {
	width: 90%;
	max-width: 400px;
	border: 5px solid #FFA000;
	border-radius: 3px;
	box-shadow:  10px 10px 50px 15px rgba(black, 0.3);
	background-color: #455A64;
	position: relative;
}

.canvas-wrapper {
	width: 100%;
	height: 0;
	padding-bottom: 100%;
	position: relative;
}

canvas {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	height: 100%;
	width: 100%;
	background-color: #37474F;
}

.controls {
	width: 100%;
	padding-top: 20px;
	padding-bottom: 0;
	display: flex;
	justify-content: space-between;
	flex-wrap: wrap;
}

button[class^="btn"] {
	background-color: #FFA000;
	padding: 5px 20px;
	text-transform: uppercase;
	font-family: 'Varela Round';
	font-size: 14px;
	color: white;
	font-weight: bold;
	border: none;
	border-radius: 3px;
	letter-spacing: 0.3em;
	height: 30px;
	line-height: 30px;
	padding: 0 10px;
	box-shadow: 0 0 10px 5px rgba(black, 0.2);
	outline: none;
	cursor: pointer;
	transition: box-shadow 0.3s, background 0.3s;
	text-align: center;
	
	margin: 0 20px 0;
	margin-bottom: 20px;
	&:hover {
		background: lighten(#FFA000, 10%);
		box-shadow: 0 0 20px 5px rgba(black, 0.3);
	}
	
	@media(min-width: 450px) {
		& {
			width: 150px;
		}
	}
}

button[disabled] {
	background: darken(#FFA000, 10%);
}

.range {
	width: 100%;
	margin-bottom: 20px;
	margin: 0 20px 20px;
	
	display: flex;
	align-items: center;
	
	label {
		color: white;
		text-transform: uppercase;
		margin-right: 20px;
		width: 30%;
	}
	
	&_inputWrapper {
		width: 100%;
		position: relative;
		height: 20px;
		background-color: #fff;		
		box-shadow: 0 0 10px 5px rgba(black, 0.2);
	}
	
	&_inputValue {
		position: absolute;
		left: 0;
		top: 0;
		height: 100%;
		color: white;
		font-size: 14px;
		height: 100%;
		padding-left: 3px;
		line-height: 20px;
	}
	
	&_inputSlider {
		transform-origin: left top;
		position: absolute;
		top: 0;
		left: 0;
		background: #FFA000;
		display: inline-block;
		width: 100%;
		height: 100%;
		transform: scaleX(0);
		transition: transform 0.2s ease-out;
	}
	
	input {	
		position: absolute;
		left: 0;
		top: 0;
		height: 100%;
		width: 100%;
		margin: 0;
		opacity: 0;
		cursor: ew-resize;
		z-index: 1
	}
}

.score {
	position: absolute;
	top: 10px;
	left: 0;
	z-index: 1;
	width: 100%;
	text-align: center;
	font-weight: bold;
	font-size: 30px;
	opacity: 0.3;
	text-transform: uppercase;
	color: white;
}

.dpad {
	width: 100px;
	margin: 0 auto;
	margin-bottom: 20px;
	display: none;
}

button[class^="control"] {
	background: #FFA000;
	border: none;
	border-radius: 3px;
	
	
}

.control-up {
	display: block;
}
.control-down svg {
	transform: rotate(180deg)
}
.control-left svg {
	transform: rotate(90deg)
}
.control-right svg {
	transform: rotate(-90deg)
}
View Compiled
let cellsNo = 20
let cellSize = 400 / cellsNo
let difficulty = 1

let score = 0

const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')

const btnStart = document.querySelector('.btn-start')
const btnPause = document.querySelector('.btn-pause')
const scoreVal = document.querySelector('.score_val')


let direction
const DIR = {
	LEFT: 37,
	UP: 38,
	RIGHT: 39,
	DOWN: 40
}

// ctx.strokeStyle = '#616161'
ctx.strokeStyle = '#27373F'
ctx.fillStyle = '#fff'

let snake = []
let food = null
let paused = false
let needsGrowth = false

let lastUpdate, lastFood, tick
let state
let flash = false
let lastKeyPressed

function update() {
	tick = Date.now()
	
	
	if (hasCollisions()) {
		flash = true
		return
	}
	
	if (tick - lastUpdate > 500 / difficulty) {
		if (lastKeyPressed && lastKeyPressed !== direction) {
			setDirection(lastKeyPressed)
		}
		
		moveSnake()
		lastUpdate = tick
	}
	
	if (tick - lastFood > foodTreshold()) {
		putFood()
	}
	
	if (headMeetsFood()) {
		needsGrowth = true
		food = null
		putFood()
		setScore(score + difficulty)
	}
}

function foodTreshold() {
	return (5000 / difficulty) * cellsNo
}

function hasCollisions() {
	const head = snake[0]
	const check = snake.concat([])
	check.shift()
	return check.find(
		c => c.x === head.x && c.y === head.y
	)
}

function snakeContains(cell) {
	return snake.find(c => c.x === cell.x && c.y === cell.y)
}

function headMeetsFood() {
	const head = snake[0]
	return food && (head.x == food.x && head.y === food.y)
}

function moveSnake() {
	const head = snake[0]
	const next = Object.assign({}, head)
	
	switch(direction) {
		case DIR.LEFT:
			--next.x;
			break;
		case DIR.UP:
			--next.y;
			break;
		case DIR.RIGHT:
			++next.x;
			break;
		case DIR.DOWN:
			++next.y;
			break;
	}
	
	if (next.x >= cellsNo) next.x = 0
	if (next.y >= cellsNo) next.y = 0
	if (next.x < 0) next.x = cellsNo -1
	if (next.y < 0) next.y = cellsNo -1

	if (!needsGrowth) {
		snake.pop()
	}
	
	needsGrowth = false
	snake.unshift(next);
}

function putFood() {
	do {
		food = {
			x: ~~(Math.random() * (cellsNo - 1)),
			y: ~~(Math.random() * (cellsNo - 1))
		}
	} while (snakeContains(food))
	
	lastFood = tick
}

function draw() {
	ctx.clearRect(0, 0, 400, 400)
	drawCells()
	drawFood()
	if (flash && ~~(Date.now() / 100) % 2 === 0) {
		return
	}
	drawSnake()
}

function drawCells() {
	for (var i = 0; i < cellsNo; ++i)
		for (var j = 0; j < cellsNo; ++j) 
			drawCell(i, j)
}

function drawFood() {
	if (food) {	
		ctx.fillStyle = '#4FC3F7'
		fillCell(food.x, food.y)
		ctx.fillStyle = '#fff'
	}
}

function drawCell(i, j) {
	ctx.strokeRect(
		i * cellSize, 
		j * cellSize, 
		cellSize, cellSize
	)
}

function drawSnake() {
	snake.forEach(
	 ({x, y}) => fillCell(x, y)
	)
}

function fillCell(x, y) {
	ctx.beginPath()
	ctx.rect(
		x * cellSize, 
		y * cellSize, 
		cellSize, cellSize
	)
	
	ctx.closePath()
	ctx.fill()
	ctx.stroke()
} 

function setScore(next) {
	score = next
	scoreVal.textContent = score
}

function startGame() {
	btnStart.textContent = 'restart'
	flash = false
	lastKeyPressed = null
	food = null
	setScore(0)
	direction = DIR.LEFT
	lastFood = lastUpdate = Date.now()
	paused = false
	setTimeout(putFood, 1000)
	const startX = cellsNo/2
	snake = [startX, startX+1, startX+2, startX+3].map(
		x => ({x, y: 15})
	)
}

function loop() {
	requestAnimationFrame(loop)
	draw()
	
	if (paused) return
	update()
}

requestAnimationFrame(loop)

btnStart.addEventListener('click', startGame)
btnPause.addEventListener('click', pause)

function pause() {
	paused = !paused
	btnPause.textContent = paused ? 'resume' : 'pause'
}

window.addEventListener('keydown', onKeyDown)
function onKeyDown({keyCode}) {
	switch (true) {
		case (keyCode === DIR.DOWN && direction === DIR.UP):
		case (keyCode === DIR.UP && direction === DIR.DOWN):	
		case (keyCode === DIR.LEFT && direction === DIR.RIGHT):
		case (keyCode === DIR.RIGHT && direction === DIR.LEFT):
			return 
	}
	
	lastKeyPressed = keyCode
}

function setDirection(keyCode) {
	direction = keyCode
}

function checkFood() {
	if (!food) return
	
	if (food.x >= cellsNo) {
		food.x = cellsNo -1
	}
	
	if (food.y >= cellsNo) {
		food.y = cellsNo -1
	}
}

class RangeSlider {
	constructor(el, cb) {
		this.input = el.querySelector('input')
		this.slider = el.querySelector('.range_inputSlider')
		this.value = el.querySelector('.range_inputValue')
		
		this.input.addEventListener('input', _ => this.onChange())
		this.input.addEventListener('keydown', e => {
			e.preventDefault()	
		})
		
		this.onChangeCallback = cb
		this.onChange()
	}
	
	onChange() {
		this.value.textContent = this.input.value
		this.slider.style.transform = `scaleX(${(this.input.value / this.input.step) / 10})`
		this.onChangeCallback(this.input.value)
	}
}


new RangeSlider(
	document.querySelector('.range-difficulty'), 
	value => difficulty = Number(value)
)
	
new RangeSlider(
	document.querySelector('.range-columns'),
	value => {
		cellsNo = Number(value)
		cellSize = 400 / cellsNo
		checkFood()
	}
)


// --- TOUCH CONTROLS
var isPointerDown, pointerStart, pointerPos

function onTouchStart(e) {
	const {clientX, clientY} = e.touches[0]
    isPointerDown = true
	pointerStart = {x: clientX, y: clientY}
	pointerPos = Object.assign({}, pointerStart)
}

function onTouchMove(e) {
	const {clientX, clientY} = e.touches[0]
    pointerPos = {x: clientX, y: clientY}
}

function onTouchEnd() {
	if (!isPointerDown) return
	isPointerDown = false
	
	const deltaX = pointerStart.x - pointerPos.x
	const deltaY = pointerStart.y - pointerPos.y
	const keyCode = touchToKeyCode(deltaX, deltaY)
	
	if (keyCode) onKeyDown({keyCode})
}
 
function touchToKeyCode(x, y) {
	if (Math.abs(x) > Math.abs(y)) {
		if (x < -1) {
			keyCode = DIR.RIGHT
		} else if (x > 1) {
			keyCode = DIR.LEFT
		} 
	} else {
		if (y < -1) {
			keyCode = DIR.DOWN
		} else if (y > 1) {
			keyCode = DIR.UP
		}
	}
	
	return keyCode
}


canvas.addEventListener('touchstart', onTouchStart)
window.addEventListener('touchmove', onTouchMove)
window.addEventListener('touchend', onTouchEnd)
View Compiled

External CSS

  1. https://fonts.googleapis.com/css?family=Varela+Round

External JavaScript

This Pen doesn't use any external JavaScript resources.