<div id="container">
   <div id="board-container">
        <div class="shine-container">
            <div class="shine"></div>
        </div>
        <div id="board"></div>
          
        <div class="info-container">
            <button id="start-button" class="state-driven"><span class="re state-driven">RE</span>START</button>
            <div class="flex"></div>
            <div id="score">0</div>
        </div>
    </div> 
    <div class="controls">
        <span class="keyboard"> Use keyboard arrow keys to control</span>
        <span class="touch">Swipe up, down, left or right to control.</span>
    </div>
</div>

<a href="https://github.com/ste-vg/snake" target="_blank" class="github-corner" aria-label="View source on Github"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
$color-background: #7ca256;
$color-background-dark: darken(#7ca256, 1%);
$color-snake: #212121;

html, body
{
	width: 100%;
	height: 100%;
	margin: 0;
	padding: 0;
	background: #d2e0d2;
	overflow: hidden;
	
	
}

@import url('https://fonts.googleapis.com/css?family=VT323');

$tilt-amount: 10deg;
$shine-move: 40px;
$snake-lift: 30px;

h1
{
	margin: 0;
}

#container
{
	
	
    display: flex;
	flex-direction: column;
    align-items: center;
  	justify-content: center;
	margin: 0;
	padding: 0;
	height: 100%;
    width: 100%;
    text-transform: uppercase;
	font-family: 'VT323', monospace;

	perspective: 1000px;
	

	#board-container
	{
		
		border-radius: 8px;
		margin: 20px;
		padding: 20px 20px 10px 20px;
		//box-shadow: 9px 7px 30px -6px rgba(0,0,0,0.25);
		box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
		background-color: white;
		background-image: radial-gradient(farthest-corner at 10px 10px, $color-background 0%, $color-background-dark 100%);
		transform: rotateX(0deg);
		transform-style: preserve-3d;
		transition: transform 0.3s ease, box-shadow 0.5s ease;
		//overflow: hidden;

		#board
		{
			border-radius: 5px;
			border: solid 1px $color-snake;
			transform: translateZ($snake-lift);
		}

		.shine-container
		{
			position: absolute;
			top: 0; 
			left: 0;
			right: 0; 
			bottom: 0;
			border-radius: 8px;
			overflow: hidden;

			.shine
			{
				position: absolute;
				top: -$shine-move; 
				left: -$shine-move;
				right: -$shine-move; 
				bottom: -$shine-move;
				background: linear-gradient(45deg, rgba(255,255,255,.75) 0%,rgba(255,255,255,0) 60%);
				transition: transform 0.3s ease;
			}
		}

		.info-container
		{
			height: 100%;
			width: 100%;
			min-height: 50px;
			margin-top: 5px;
			display: flex;
			flex-direction: row;
			//align-items: center;
			align-content: center;
			justify-content: space-between;
			transform: translateZ(20px);

			.label
			{
				margin: 5px 5px 0 0;
			}

			#score
			{
				font-size: 1.5em;
				font-weight: 300;
				padding: 10px 20px;
			}

			.flex
			{
				flex: 1;
			}
		}
	}

	&.PLAYING
	{
		#board-container
		{
			box-shadow: 0 45px 100px rgba(0, 0, 0, 0.3);
			transform: translateZ(40px);

			&.up
			{ 
				transform: rotateX($tilt-amount); 
				.shine-container .shine { transform: rotateX($tilt-amount) translateX(-$shine-move) translateZ(1px) } 
			}
			&.down
			{ 
				transform: rotateX(-$tilt-amount); 
				.shine-container .shine { transform: rotateX(-$tilt-amount) translateX($shine-move) translateZ(1px) } 
			}
			&.left
			{ 
				transform: rotateY(-$tilt-amount); 
				.shine-container .shine { transform: rotateY(-$tilt-amount) translateY(-$shine-move) translateZ(1px) } 
			}
			&.right
			{ 
				transform: rotateY($tilt-amount); 
				.shine-container .shine { transform: rotateY($tilt-amount) translateY($shine-move) translateZ(1px) } 
			}
		}
	}

	#start-button
	{
		font-family: inherit;
		text-transform: uppercase;
		font-size: 1.5em;
		background-color: transparent;
		color: $color-snake;
		padding: 10px 20px;
		//border: 2px solid $color-snake;
		border: 0;
		border-radius: 2px;
		//margin: 10px 10px 40px 10px;
		cursor: pointer;
		outline: none;

		&:hover
		{
			color: white;
		//	background-color: $color-background;
			border-color: $color-background;
		}
	}

	.state-driven
	{
		display: none;
	}

	&.READY
	{
		#start-button
		{
			display: block;
		}
	}

	&.ENDED
	{
		#start-button
		{
			display: block;
		}

		.re
		{
			display: inline;
		}
	}

	.controls
	{
		.keyboard{ display: inline; }
		.touch{ display: none; }

		@media (any-hover: none) and (any-pointer: coarse) 
		{ 
			.keyboard{ display: none; }
			.touch{ display: inline; }
		}

	}
}

@keyframes flash 
{
    0% { opacity: 1 }
    50% { opacity: 0 }
}

#board 
{
	--grid-columns: 0;
	--grid-rows: 0;
    --grid-size: 0;

    width: calc(var(--grid-size) * var(--grid-columns) * 1px);
    height: calc(var(--grid-size) * var(--grid-rows) * 1px);
	display: grid;
	grid-template-columns: repeat(var(--grid-columns), 1fr);
 	grid-template-rows: repeat(var(--grid-rows), 1fr);
    grid-gap: 1px;
    
	>div
	{
        background-color: transparent;

        &.food, &.snake
        {
            box-shadow: 0px 0px 0px rgba(0,0,0,0.1);
            transition: box-shadow 0.3s ease;
        }

        &.food
        {
            background-color: $color-snake;
            border-radius: 50%;
            margin: 1px;
        }
        
        &.snake
        {
            background-color: $color-snake;

            &.head
            {
                &.up{ border-top-left-radius: 50%; border-top-right-radius: 50%;}
                &.down{ border-bottom-left-radius: 50%; border-bottom-right-radius: 50%;}
                &.left{ border-top-left-radius: 50%; border-bottom-left-radius: 50%;}
                &.right{ border-top-right-radius: 50%; border-bottom-right-radius: 50%;}
            }

            &.tail
            {
                &.up{ border-bottom-left-radius: 50% 100%; border-bottom-right-radius: 50% 100%;}
                &.down{ border-top-left-radius: 50% 100%; border-top-right-radius: 50% 100%;}
                &.right{ border-bottom-left-radius: 100% 50%; border-top-left-radius: 100% 50%;}
                &.left{ border-bottom-right-radius: 100% 50%; border-top-right-radius: 100% 50%;}
            }

            &.turn-left
            {
                &.up{ border-top-right-radius: 50%;}
                &.down{ border-bottom-right-radius: 50%;}
            }
            
            &.turn-right
            {
                &.up{ border-top-left-radius: 50%;}
                &.down{ border-bottom-left-radius: 50%;}
            }
            
            &.turn-up
            {
                &.left{ border-bottom-left-radius: 50%;}
                &.right{ border-bottom-right-radius: 50%;}
            }
            
            &.turn-down
            {
                &.left{ border-top-left-radius: 50%;}
                &.right{ border-top-right-radius: 50%;}
            }

            &.dead
            {
                animation: flash 0.3s steps(1) infinite;
            }
        }  
	}
}

$snake-shadow: 5px;

.up #board >div
{
    &.food, &.snake { box-shadow: 0px $snake-shadow 0px 0px rgba(0,0,0,0.1);}
}

.down #board >div
{
    &.food, &.snake { box-shadow: 0px (-$snake-shadow) 0px 0px rgba(0,0,0,0.1);}
}

.left #board >div
{
    &.food, &.snake { box-shadow: $snake-shadow 0px 0px 0px rgba(0,0,0,0.1);}
}

.right #board >div
{
    &.food, &.snake { box-shadow: -$snake-shadow 0px 0px 0px rgba(0,0,0,0.1);}
}
View Compiled


// Also on Github with Webpack and better Typescript support
// https://github.com/ste-vg/snake



console.clear();

enum GAME_STATES
{
	ready = 'READY',
	playing = 'PLAYING',
	ended = 'ENDED',
	paused = 'PAUSED'
}

enum SOUND
{
	move = 'move',
	dead = 'dead',
	collect = 'collect',
	start = 'start'
}

interface Direction
{
    name: string;
    x: number;
    y: number;
}

interface Position
{
    x: number;
    y: number;
}

interface SnakePart
{
    position: Position;
    direction: Direction;
}

interface States
{
    direction: Direction;
    nextDirection: Direction[];
    speed: number;
    game: string;
    timeStamp: number;
    snakeLength: number;
    score: number;
}

class App
{
    private game:Snake;
    private score:HTMLElement;
    private container:HTMLElement;
    private boardContainer:HTMLElement;
    private gameState:string;

    constructor()
    {  
        this.setupUI();
        this.setupGame();
    }

    setupUI()
    {
        this.score = document.getElementById('score');
        this.container = document.getElementById('container');
        this.boardContainer = document.getElementById('board-container');
        let startButton = Rx.Observable.fromEvent(document.getElementById('start-button'), 'click');
        startButton.subscribe((e:MouseEvent) => { console.log('click'); this.startGame(); })
    }

    setupGame()
    {
        let board = document.getElementById('board');

        this.game = new Snake(board);
        this.game.score.subscribe((score:number) => this.score.innerHTML = String(score));
        this.game.state.subscribe((state:string) => 
        {
            this.gameState = state;
            this.container.setAttribute('class', state)
        })
        this.game.direction.subscribe((direction:string) => this.boardContainer.setAttribute('class', direction))
        this.game.reset();
    }

    startGame()
    {
        if(this.gameState == GAME_STATES.ready || this.gameState == GAME_STATES.ended)
        {
            this.game.start();
        }
    }
}

class Snake
{
	private SETTINGS = {
		grid: {size: 10, rows: 20, columns: 28},
		game: {scoreIncrement: 10},
		snake: {startLength: 3, startSpeed: 300, speedIncrement: 10, minSpeed: 100, growBy: 2}
	}

	private DIRECTION = {
		up: 	{name: 'up', 	x: 0, 	y: -1},
		down: 	{name: 'down', 	x: 0, 	y: 1},
		left: 	{name: 'left', 	x: -1, 	y: 0},
		right: 	{name: 'right', x: 1, 	y: 0},
	}

	private states:States = {
		direction: this.DIRECTION.up,
		nextDirection: [this.DIRECTION.up],
		speed: 0,
		game: GAME_STATES.ready,
		timeStamp: 0,
		snakeLength: 0,
		score: 0
	}

	//http://loov.io/jsfx

	private sfxLibrary:any = {
		"start":{
			"Frequency":{"Start":463.2977575242697,"Slide":0.4268311992714056,"RepeatSpeed":0.6870767779635416},
			"Generator":{"A":0.015696072909390766},
			"Volume":{"Sustain":0.11353385475559997,"Decay":0.15242709930669884}
		},
		"collect1":{
			"Frequency":{"Start":1183.9224793246758,"ChangeSpeed":0.12793431035602038,"ChangeAmount":4.8612434857196085},
			"Volume":{"Sustain":0.011448880380128946,"Decay":0.3895997546965799,"Punch":0.4554389528366015}
		},
		"collect2":{
			"Frequency":{"Start":1070.9337014976563,"ChangeSpeed":0.1375978771153015,"ChangeAmount":5.9409661118536246},
			"Volume":{"Sustain":0.04890791064198004,"Decay":0.3415421194668815,"Punch":0.46291381941601983}
		},
		"dead":{
			"Frequency":{"Start":194.70758491034655,"Slide":-0.011628522004559189,"ChangeSpeed":0.6591296059731018,"ChangeAmount":2.6287197798189297},
			"Generator":{"Func":"noise"},
			"Volume":{"Sustain":0.17655222296084297,"Decay":0.24077933399701645,"Punch":0.6485369099751499}
		},
		"move1":{
			"Frequency":{"Start":452,"Slide":-0.04,"Min":30,"DeltaSlide":-0.05},
			"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
			"Filter":{"HP":0.10068425608105215},
			"Volume":{"Sustain":0,"Decay":0.041,"Attack":0.011,"Punch":0.04,"Master":0.18}
		},
		"move2":{
			"Frequency":{"Start":452,"Slide":-0.01,"Min":30,"DeltaSlide":-0.05},
			"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
			"Filter":{"HP":0.26,"LPResonance":0,"HPSlide":0.35,"LPSlide":0.51,"LP":1},
			"Volume":{"Sustain":0.02,"Decay":0.001,"Attack":0.021,"Punch":0.05,"Master":0.18},
			"Phaser":{"Offset":-0.03,"Sweep":-0.02},
			"Vibrato":{"FrequencySlide":0.04,"Frequency":14.01,"Depth":0.06}
		},
		"move3":{
			"Frequency":{"Start":452,"Slide":-0.01,"Min":30,"DeltaSlide":-0.05},
			"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
			"Filter":{"HP":0.26,"LPResonance":0,"HPSlide":0.35,"LPSlide":0.51,"LP":1},
			"Volume":{"Sustain":0.02,"Decay":0.001,"Attack":0.021,"Punch":0.05,"Master":0.18},
			"Phaser":{"Offset":-0.03,"Sweep":-0.02},
			"Vibrato":{"FrequencySlide":0.04,"Frequency":14.01,"Depth":0.16}
		},
		"move4":{
			"Frequency":{"Start":452,"Slide":-0.01,"Min":30,"DeltaSlide":-0.05},
			"Generator":{"Func":"sine","A":0.08999657142884616,"ASlide":0.3390436675524937},
			"Filter":{"HP":0.26,"LPResonance":0,"HPSlide":0.35,"LPSlide":0.51,"LP":1},
			"Volume":{"Sustain":0.02,"Decay":0.001,"Attack":0.021,"Punch":0.05,"Master":0.18},
			"Phaser":{"Offset":-0.03,"Sweep":-0.02},
			"Vibrato":{"FrequencySlide":0.04,"Frequency":14.01,"Depth":0.27}
		}
	}

	private player:any = jsfx.Sounds(this.sfxLibrary);

	private sounds:any = {
		collect: ['collect1', 'collect2'],
		dead: ['dead'],
		start: ['start'],
		move: ['move1', 'move2', 'move3', 'move4']
	}

	private board:HTMLElement;
	private grid:HTMLElement[] = [];
	private snake:SnakePart[] = [];
	private food:Position;
	private touchStartPosition:Position;

	// subjects

	public state:Subject<string> = new Rx.Subject();
	public score:Subject<number> = new Rx.Subject();
	public direction:Subject<string> = new Rx.Subject();

	// observables

	private keyPress:Observable<any>;
	private input:Input;

	// subscriptions
	private keyPressSubscription:Subscription;
	private touchStartSubscription:Subscription;
	private touchEndSubscription:Subscription;
	private keyRestartSubscription:Subscription;

	constructor(boardElement: HTMLElement)
	{
		this.board = boardElement;
		
		// setup the game board grid
		
		this.board.style.setProperty("--grid-size", String(this.SETTINGS.grid.size));
		this.board.style.setProperty("--grid-columns", String(this.SETTINGS.grid.columns));
		this.board.style.setProperty("--grid-rows", String(this.SETTINGS.grid.rows));
		
		let count = this.SETTINGS.grid.columns * this.SETTINGS.grid.rows;
		for(let i = 0; i < count; i++)
		{
			let sq = document.createElement("div");
			this.grid.push(sq);
			this.board.appendChild(sq);
		}

		// setup observables

		this.input = new Input(document.body);
	
		this.keyPress = Rx.Observable.fromEvent(document, "keydown")
			.filter((e:KeyboardEvent) => ['arrowright', 'arrowleft', 'arrowup', 'arrowdown'].indexOf(e.key.toLowerCase()) >= 0)
			.map((e:KeyboardEvent) => 
			{
				e.preventDefault();
				return e.key.toLowerCase().replace('arrow','')
			})

		let onEnter = Rx.Observable.fromEvent(document, "keydown")
			.filter((e:KeyboardEvent) => ['enter'].indexOf(e.key.toLowerCase()) >= 0)
				
		this.touchStartSubscription = this.input.starts.subscribe((position:Position) => {
			this.touchStartPosition = position;
		})

		this.touchEndSubscription = this.input.ends.subscribe((position:Position) => 
		{
			let hDiff = this.touchStartPosition.x - position.x;	
			let hDiffAbs = Math.abs(hDiff);	
			let vDiff = this.touchStartPosition.y - position.y;
			let vDiffAbs = Math.abs(vDiff);
			
			if(hDiffAbs > 10 || vDiffAbs > 10)
			{
				if(hDiffAbs > vDiffAbs)
				{
					if(hDiff < 0) this.setDirection(this.DIRECTION['right']);
					else this.setDirection(this.DIRECTION['left']);
				}
				else
				{
					if(vDiff < 0) this.setDirection(this.DIRECTION['down']);
					else this.setDirection(this.DIRECTION['up']);
				}
			}
		})			

		this.keyPressSubscription = this.keyPress.subscribe((key: string) => 
		{
			if(this.states.game == GAME_STATES.playing)
			{
				this.setDirection(this.DIRECTION[key])
			}
		})

		this.keyRestartSubscription = onEnter.subscribe(e => this.start())
	}

	private playSound(type:SOUND)
	{
		let options = this.sounds[type];
		let selected = options[Math.floor(Math.random() * options.length)];
		this.player[selected]();
	}
	
	private checkDirection(setDirection:Direction, newDirection:Direction):boolean
	{
		return setDirection.x != newDirection.x && setDirection.y != newDirection.y;	
	}

	private setDirection(direction:Direction)
	{
		let queueable:boolean = false;

		if(this.states.direction.name != this.states.nextDirection[0].name)
		{
			//if a valid move we could queue this move
			if(this.states.nextDirection.length == 1 && this.checkDirection(this.states.nextDirection[0], direction))
			{
				queueable = true;
			}
		}

		if(queueable && this.checkDirection(this.states.nextDirection[0], direction)) 
		{
			this.states.nextDirection.push(direction);
			this.playSound(SOUND.move);
		}
		else if(this.checkDirection(this.states.direction, direction)) 
		{
			this.states.nextDirection = [direction];
			this.playSound(SOUND.move);
		}
	}

	public reset()
	{
		this.updateGameState(GAME_STATES.ready);

		this.snake = []
		this.states.direction = this.DIRECTION.up;
		this.states.nextDirection = [this.DIRECTION.up];
		this.states.snakeLength = this.SETTINGS.snake.startLength;
		this.updateScore(0);
		let center:Position = {x: Math.round(this.SETTINGS.grid.columns / 2), y: Math.round(this.SETTINGS.grid.rows / 2)};

		for(let i = 0; i < this.states.snakeLength; i++)
		{
			let snakePart:SnakePart = {
				position: {x: center.x, y: center.y + (i * 1)},
				direction: this.DIRECTION.up
			}

			this.snake.unshift(snakePart);
		}

		this.placeFood();

		this.draw();
	}

	private draw()
	{
		// reset all sqaures
		for(let i = 0; i < this.grid.length; i++) this.grid[i].className = '';

		// set snake squares
		for(let i = 0; i < this.snake.length; i++)
		{
			let classes = ['snake'];
			if(this.states.game == GAME_STATES.ended) classes.push('dead');
			if(i == 0) classes.push('tail');
			if(i == this.snake.length - 1) classes.push('head');
			let snakePart = this.snake[i];
			let nextSnakePart = this.snake[i + 1] ? this.snake[i + 1] : null;
			
			if(nextSnakePart && snakePart.direction.name != nextSnakePart.direction.name)
			{
				classes.push('turn-' + nextSnakePart.direction.name)
			}
			
			if(i == 0 && nextSnakePart)
			{
				classes.push(nextSnakePart.direction.name);
			}
			else
			{
				classes.push(snakePart.direction.name);
			}
			let gridIndex = this.getIndexFromPosition(snakePart.position);
			this.grid[gridIndex].className = classes.join(' ');
		}

		// set food sqaure

		let foodSquare = this.grid[this.getIndexFromPosition(this.food)];
		foodSquare.className = 'food';
	}

	private getIndexFromPosition(position:Position):number
	{
		return position.x + (position.y * this.SETTINGS.grid.columns);
	}

	private getPositionFromIndex(index:number):Position
	{
		let y = Math.floor(index / this.SETTINGS.grid.columns);
		let x = Math.floor(index % this.SETTINGS.grid.columns);
		return {x: x, y: y};
	}

	private eatFood()
	{
		this.addScore();
		this.playSound(SOUND.collect);
		this.states.snakeLength += this.SETTINGS.snake.growBy;
		this.states.speed -= this.SETTINGS.snake.speedIncrement;
		if(this.states.speed < this.SETTINGS.snake.minSpeed) this.states.speed = this.SETTINGS.snake.minSpeed;
		this.placeFood();
		
	}

	private updateGameState(newState:string)
	{
		this.states.game = newState;
		this.state.next(this.states.game);
	}

	private addScore()
	{
		this.updateScore(this.states.score + this.SETTINGS.game.scoreIncrement);
	}
	
	private updateScore(newScore:number)
	{
		this.states.score = newScore;
		this.score.next(this.states.score);
	}

	private placeFood()
	{
		let takenSpaces: number[] = [];
		for(let i = 0; i < this.snake.length; i++)
		{
			let index = this.getIndexFromPosition(this.snake[i].position);
			takenSpaces.push(index);
		}

		let availableSpaces: number[] = [];
		for(let i = 0; i < this.grid.length; i++)
		{
			if(takenSpaces.indexOf(i) < 0) availableSpaces.push(i);
		}

		let i = Math.floor(Math.random() * availableSpaces.length);
		this.food = this.getPositionFromIndex(availableSpaces[i]);
	}

	private tick(timeStamp:number)
	{
		if(this.states.game == GAME_STATES.playing)
		{
			if(!this.states.timeStamp || (timeStamp - this.states.timeStamp) > this.states.speed)
			{
				this.states.timeStamp = timeStamp;
				if(this.states.nextDirection.length > 1)
				{
					this.states.direction = this.states.nextDirection.shift();
				}
				else
				{
					this.states.direction = this.states.nextDirection[0];
				}
				this.direction.next(this.states.nextDirection[this.states.nextDirection.length - 1].name);

				let snakeHead = this.snake[this.snake.length - 1];
				let newPosition:Position = {
					x: snakeHead.position.x + this.states.direction.x,
					y: snakeHead.position.y + this.states.direction.y
				}

				// end the game if the new postion is out of bounds

				if(	newPosition.x < 0 || 
					newPosition.x > this.SETTINGS.grid.columns - 1 || 
					newPosition.y < 0 || 
					newPosition.y > this.SETTINGS.grid.rows - 1)
				{
					return this.end();
				}

				// end the game if the new position is already taken by snake

				for(let i = 0; i < this.snake.length; i++)
				{
					if(this.snake[i].position.x == newPosition.x && this.snake[i].position.y == newPosition.y)
					{
						return this.end();
					}
				}

				// all good to proceed with new snake head

				let newSnakeHead:SnakePart = {
					position: newPosition,
					direction: this.DIRECTION[this.states.direction.name]
				}
				this.snake.push(newSnakeHead);

				while(this.snake.length > this.states.snakeLength)
				{
					this.snake.shift();
				}

				// check if head is on food

				if(newSnakeHead.position.x == this.food.x && newSnakeHead.position.y == this.food.y)
				{
					this.eatFood();
				}

				this.draw();
			}

			window.requestAnimationFrame(time => this.tick(time));
		}
	}

	public start()
	{
		this.reset();
		this.playSound(SOUND.start);
		this.states.speed = this.SETTINGS.snake.startSpeed;
		this.updateGameState(GAME_STATES.playing);
		this.tick(0);
		window.focus();
	}

	private end()
	{
		console.warn('GAME OVER')
		this.playSound(SOUND.dead);
		this.updateGameState(GAME_STATES.ended);
		this.direction.next('');
		this.draw();
	}
}

// touch & mouse input code form https://codepen.io/HunorMarton/post/handling-complex-mouse-and-touch-events-with-rxjs

class Input
{
    private mouseDowns:Observable<Position>;
    private mouseMoves:Observable<Position>;
    private mouseUps:Observable<Position>;

    private touchStarts:Observable<Position>;
    private touchMoves:Observable<Position>;
    private touchEnds:Observable<Position>;

    public starts:Observable<Position>;
    public moves:Observable<Position>;
    public ends:Observable<Position>;

    constructor(element:HTMLElement)
    {
        this.mouseDowns = Rx.Observable.fromEvent(element, "mousedown").map(this.mouseEventToCoordinate);
        this.mouseMoves = Rx.Observable.fromEvent(window, "mousemove").map(this.mouseEventToCoordinate);
        this.mouseUps = Rx.Observable.fromEvent(window, "mouseup").map(this.mouseEventToCoordinate);

        this.touchStarts = Rx.Observable.fromEvent(element, "touchstart").map(this.touchEventToCoordinate);
        this.touchMoves = Rx.Observable.fromEvent(element, "touchmove").map(this.touchEventToCoordinate);
        this.touchEnds = Rx.Observable.fromEvent(window, "touchend").map(this.touchEventToCoordinate);

        this.starts = this.mouseDowns.merge(this.touchStarts);
        this.moves = this.mouseMoves.merge(this.touchMoves);
        this.ends = this.mouseUps.merge(this.touchEnds);
    }

    private mouseEventToCoordinate = (mouseEvent:MouseEvent) => 
    {
        mouseEvent.preventDefault();
        return {
            x: mouseEvent.clientX, 
            y: mouseEvent.clientY
        };
    };

    private touchEventToCoordinate = (touchEvent:TouchEvent) => 
    {
        //touchEvent.preventDefault();
        return {
            x: touchEvent.changedTouches[0].clientX, 
            y: touchEvent.changedTouches[0].clientY
        };
    };
}

let app = new App();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //codepen.io/steveg3003/pen/zBVakw.js
  2. https://s3-us-west-2.amazonaws.com/s.cdpn.io/557388/jsfx.js
  3. https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.1/Rx.min.js