<div id="app">
  <transition v-on:enter="enter" v-bind:css="false" appear>
    <div class="butter-cheese-eggs" v-show="true">
      <div v-for="(block, index) in grid" @click="select(index)">
        <block :figure.sync="block.figure" />
      </div>
    </div>
  </transition>
  
  <transition v-on:enter="enterWin" v-bind:css="false">
    <win v-show="winner" :click-handler="restart"></win>
  </transition>
</div>

<template id="block">
  <div class="block">
    <transition v-on:enter="enter" v-bind:css="false">
      <span v-show="figure > -1">{{ fig }}</span>
    </transition>
  </div>
</template>

<template id="win">
  <div class="win">
    <h2>Win</h2>
    <!--<button @click="clickHandler">Play again</button>-->
  </div>
</template>
@import url('https://fonts.googleapis.com/css?family=Permanent+Marker');

* {
  box-sizing: border-box;
}

#app {
  display: flex;
  overflow: hidden;
  position: relative;  
  width: 100vw;
  height: 100vh;
  background-image: linear-gradient(red, orange);
}

.butter-cheese-eggs {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-wrap: wrap;
  width: 300px;
  height: 300px;
  
  > div {
    flex: 0 0 auto;
    width: 100px;
    height: 100px;
    
    .block {
      cursor: pointer;
      display: table-cell;
      width: 100px;
      height: 100px;
      font: bold 50px/0 'Comic Sans MS', sans-serif;
      vertical-align: middle;
      text-align: center;
    }
    
    &:nth-child(1),
    &:nth-child(2),
    &:nth-child(3),
    &:nth-child(4),
    &:nth-child(5),
    &:nth-child(6) {
      border-bottom: 3px solid #fff;
    }

    &:nth-child(2),
    &:nth-child(5),
    &:nth-child(8) {
      border-left: 3px solid #fff;
      border-right: 3px solid #fff;
    }
  }
  
  span {
    display: block;
  }
}

.win {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) rotate(-10deg);
  transform-origin: 50% 50%;
  transform-origin: center;
  width: 300px;
  height: 300px;
  margin-top: -60px;
  margin-left: -20px;
  padding-top: 140px;
  font-family: 'Permanent Marker', cursive;
  text-align: center;
  
  h2 {
    display: block;
    margin: 0 0 10px;
    font-size: 100px;
    text-shadow: 0px 0px 50px white;
    text-align: center;
  }
  
  button {
    cursor: pointer;
    transition: all 200ms cubic-bezier(0.175, 0.885, 0.320, 1.275);
    outline: none;
    display: inline-block;
    padding: 15px;
    background-color: darkred;
    border: none;
    border-radius: 10px;
    font-family: 'Permanent Marker', cursive;
    font-size: 20px;
    color: white;
    
    &:hover {
      transform: scale(1.1);
    }
  }
}
View Compiled
let state = {
  grid: _.map(_.range(0, 9), (index) => {
    return { index, figure: -1 }
  }),
  myTurn: false
}

const appState = _.cloneDeep(state)

const block = Vue.component('block', {
  name: 'block',
  
  template: '#block',
  
  props: {
    figure: {
      type: Number,
      default: -1
    }
  },
  
  computed: {
    fig () {
      return this.figure === 0 ? 'O' : 'X'
    }
  },
  
  data () {
    return {
      selected: false
    }
  },
  
  methods: {
    enter (el, done) {
      TweenMax.from(el, 1, {
        autoAlpha: 0,
        scale: 0,
        ease: Elastic.easeOut.config(1.25, 0.5),
        onComplete: done
      })
    }
  }
}) 

const win = Vue.component('win', {
  name: 'win',
  template: '#win',
  props: {
    clickHandler: {
      type: Function,
      default: null
    }
  }
})

const app = new Vue({
  name: 'app',
  
  el: '#app',
  
  data() {
    return state
  },
  
  components: {
    block
  },
  
  computed: {
     winner () {
       const wins = ['012', '036', '345', '147', '258', '678','048', '246']
       const grid = this.grid
       const player = this.myTurn ? 0 : 1
       const moves = _.reduce(this.grid, (result, value, index) => {
         if (value.figure === player) {
           result.push(index)
         }
         
         return result
       }, [])
       
       return !!_.find(wins, win => {
          const combination = _.map(win.split(''), n => parseInt(n));
          console.log('combination', combination, moves)
         
          return _.difference(combination, moves).length === 0;
       })
     }
  },
  
  methods: {
    select (index) {
      const {figure} = this.grid[index]
      
      if (figure > -1) {
        return;
      }
      
      this.grid[index].figure = this.myTurn ? 1 : 0
      this.myTurn = !this.myTurn
    },
    
    restart () {
      this.grid = appState.grid
      this.myTurn = appState.myTurn
    },
    
    enter (el, done) {
      TweenMax.from(el, 1, {
        autoAlpha: 0,
        scale: 0,
        ease: Elastic.easeOut.config(1.25, 0.5)
      })
    },
    
    enterWin (el) {
      TweenMax.from(el, 1, {
        autoAlpha: 0,
        scale: 0,
        ease: Elastic.easeOut.config(1.25, 0.5)
      })
    }
  }
})
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js
  2. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.0/TweenMax.min.js