<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