<script type="text/x-template" id="markerSelect">
  	<transition name="modal">
  		<div class="modal-mask">
  			<div class="modal-wrapper">
  				<div class="modal-container">
  					<div class="modal-body">
  						<slot name="body">
  							<div class='row'>
  								<div class="col-xs-2 select_marker"
  										 @click='set("X")'><span>X</span></div>
  								<div class="col-xs-8 select_title">
  									SELECT<span style='color:#00B16A'>MARKER</span>
  								</div>
  								<div class="col-xs-2 select_marker"
  										 @click='set("O")'>O</div>
  							</div>
  						</slot>
  					</div>
  				</div>
  			</div>
  		</div>
  	</transition>
  </script>

  <div id='app'>
    <transition enter-active-class='animated fadeIn' 
								leave-active-class='animated fadeOut' 
								mode='out-in'>
      <modal v-if='showModal'>
      </modal>

      <div id='welcome' v-if='!started'>
        <div class='titleBlock'>
          <h1>tic<span>tac</span>toe</h1>
          <div class='row mode' 
							 @click='init("player")'
							 @mouseover='tick.play()'>
            [versus<span>player</span>]
          </div>
          <div class='row mode' 
							 @click='showModal = true'
							 @mouseover='tick.play()'>
            [versus<span>computer</span>]
          </div>
        </div>
      </div>

      <div id='main' v-if='started && !game.over'>
        <table id='board'>
          <tr v-for='row in board'>
            <td v-for='box in row'>
              <div class='square' 
									 @click='router(box)' 
									 :class='{"square active": box.bg}'>
                {{box.val}}
              </div>
            </td>
          </tr>
        </table>
      </div>

      <div id='reset' v-if='started && game.over'>
        <div class='titleBlock'>
					<h1 v-if='!game.draw'><span>{{game.winner}}</span> wins!</h1>
					<h1 v-if='game.draw'><span>draw</span></h1>
          <h2>play again?</h2>
          <div class='row mode' 
							 @click='init("player")'
							 @mouseover='tick.play()'>
            [versus<span>player</span>]
          </div>
          <div class='row mode' 
							 @click='showModal = true'
							 @mouseover='tick.play()'>
            [versus<span>computer</span>]
          </div>
        </div>
      </div>
    <!--//-->
    </transition>
  </div>
@import 'https://fonts.googleapis.com/css?family=Oswald';

$primary: #6C7A89;
$secondary: #00B16A;

$main: 'Oswald', sans-serif;

* {
	user-select: none
}

.center {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%,-50%);
}

span {
  color: $secondary;
}

//<----Modal---->//
.modal-mask {
	position: absolute;
	z-index: 9998;
	top: 0; left: 0;
	width: 100%;
	height: 100%;
	background: rgba($primary,.5);
	transition: opacity .3s ease;
}

.modal-container {
	@extend .center;
	width: 80%;
	background: white;
	border-radius: 10px;
	box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
	transition: all .3s ease;
}

.modal-body {
	font-family: $main;
	color: black;
	font-size: 8vw;
	margin: 0;
	width: 100%;
	text-align: center;
	background: $primary;
	color: white;
}

.select_marker {
  cursor: pointer;
}
//<----/Modal---->//

#welcome {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background: $primary;
}

.titleBlock {
  @extend .center;
  color: white;
  font-family: $main;
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 4px;
  h1 {
    font-size: 100px;
		width: 100%;
  }
  .mode {
    font-size: 50px;
    cursor: pointer;
  }
}

#board {
  @extend .center;
  border-collapse: separate;
  border-spacing: 2px;
}

td {
	vertical-align: middle;
}

.square {
  width: 15vw;
  height: 15vw;
  background: $primary;
  cursor: pointer;
  font-family: $main;
  font-size: 15vw;
  line-height: 15vw;
  text-align: center;
  color: white;
  &:hover {
    opacity: .8;
  }
  &.active {
    background: $secondary;
  }
}

#reset {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background: $primary;
}
View Compiled
/*
AI is half smart/half stupid by design. Originally I used the minimax algorithm, but that made for a pretty boring game, always ending in either a tie or a loss. So I made my own AI algorithm. The decision tree looks like this:
	1. Winning move available? ---> 
	2. Will lose next turn without blocking move? ---> 
	3. Choose a move at random
*/

const vm = new Vue({
  el: '#app',
  components: {
		modal: {
			template: '#markerSelect',
			methods: {
        set(marker){
          vm.markers.player = marker;
          vm.markers.computer = marker == 'X' ? 'O' : 'X';
          vm.init('computer');
        }
			}
		}
	},
  data: {
    started: false,
    showModal: false,
    mode: '',
    run: false,

    turn: 'X',

    markers: {
      player: '',
      computer: ''
    },

    game: {
      over: false,
      winner: '',
      draw: false,
    },

    board: [
      [{val:'',bg:''}, {val:'',bg:''}, {val:'',bg:''}],
			[{val:'',bg:''}, {val:'',bg:''}, {val:'',bg:''}],
			[{val:'',bg:''}, {val:'',bg:''}, {val:'',bg:''}]
    ],

    windex: [
      [[0,0], [0,1], [0,2]],
      [[1,0], [1,1], [1,2]],
    	[[2,0], [2,1], [2,2]],
  	  [[0,0], [1,0], [2,0]],
    	[[0,1], [1,1], [2,1]],
    	[[0,2], [1,2], [2,2]],
  		[[0,0], [1,1], [2,2]],
  		[[0,2], [1,1], [2,0]]
     ],
		
		tick: new Audio('https://dl.getdropbox.com/s/kgqqnei0yhv3r8n/219069__annabloom__click1.wav')
  },//DATA

  computed: {
    arr() {
			return this.board.map(x => x.map( y => y.val));
		},

		winArr() {
			return this.windex.map(x => x.map( y => this.board[y[0]][y[1]].val));
		},

    check(){
      const board = this.arr;
      const windex = this.winArr;
      const draw = this.arr.every(x => x.every(y => y != ''));

      const checkWin = windex.forEach((x,ind) => {
        const vector = this.windex[ind];
        let over = false;

        if(x.every(y => y == 'X')){
          this.game.winner = 'X';
          over = true;
        }

        if(x.every(y => y == 'O')){
          this.game.winner = 'O';
          over = true;
        }

        if(draw && this.game.winner == ''){
          this.game.draw = true;
          over = true;
        }

        if(over){
          this.run = false;
          this.setWin(vector);
        }
      })
    },

    AIWinMove(){
			let move = '';
			const board = this.board;
			const combos = this.winArr;
			const AI = this.markers.computer;
			const AIStr = AI.repeat(3);
			combos.forEach((x,ind) => {
				if(x.join('') + AI === AIStr){
					let row = combos[ind];
					let col = row.indexOf('');
					let index = this.windex[ind][col];
					let box = board[index[0]][index[1]];
					move = box;
				}
			});
			return move;
		},

		AIBlockMove(){
			let move = '';
			const board = this.board;
			const combos = this.winArr;
			const player = this.markers.player;
			const playStr = player.repeat(3);
			combos.forEach((x,ind) => {
				if(x.join('') + player === playStr){
					let row = combos[ind];
					let col = row.indexOf('');
					let index = this.windex[ind][col];
					let box = board[index[0]][index[1]];
					move = box;
				}
			});
			return move;
		},

		randomMove(){
			const board = this.board;
			let counter = 0;
			const recurse = () => {
				if(counter == 8) return;
				counter++;
				let row = Math.floor(Math.random() * 2);
				let col = Math.floor(Math.random() * 2);
				if(board[row][col].val == ''){
					return board[row][col];
				} else
					return recurse();
			};
			return recurse();
		},

    AI(){
      const win = this.AIWinMove;
      const block = this.AIBlockMove;
      const random = this.randomMove;

      if(win != '')
        return win
      else if(win == '' && block != '')
        return block
      else
        return random
    }
  },//COMPUTED

  methods: {
    init(mode){
      this.reset();
      this.mode = mode;
      this.showModal = false;

      if(mode == 'computer')
        this.turn = this.markers.player;

      this.start();
    },

    start(){
      this.run = true;
      this.started = true;
    },

    reset(){
      this.mode = '';
      this.run = false;
      this.game.over = false;
      this.game.winner = '';
      this.game.draw = false;

      this.board.forEach(x => {
        x.map(y => {y.val = ''; y.bg = ''})
      })
    },

    router(box){
      if(this.mode == 'player')
        this.mark(box);
   
      if(this.mode == 'computer' && box.val == ''){
        this.mark(box);
        setTimeout(() => {
          this.mark(this.AI);
        }, 300)
      }
    },

    mark(box){
      const run = this.run;

      if(box.val == '' && run){
        box.val = this.turn;
        this.turn = this.turn == 'X' ? 'O' : 'X';
        this.check;
      }
    },

    setWin(vector){
      if(!this.game.draw){
        vector.forEach( x => {
          this.board[x[0]][x[1]].bg = 'active'
        })
      }
			
			setTimeout(() => {
				this.game.over = true;
			}, 500)
    }
  }//METHODS
})


Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css
  2. https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js