<div class="wrapper">
  
  <div class="dialogs">
    
    <div class="cover"></div>
    
    <div class="pick">
      <h2 class="msg">Pick</h2>
      <button class="x"></button>
      <button class="o"></button>
    </div> <!-- end-pick -->
    
    <div class="end">
      <h2 class="msg"></h2>
      <button class="replay"><i class="fa fa-refresh"></i></button>
    </div> <!-- end-end -->
    
  </div> <!-- end-dialogs -->
  
  <div class="container">
    
    <div class="game">
      
      <div class="row top">
        <div class="col"></div>
        <div class="col"></div>
        <div class="col"></div>
      </div> <!-- end-row -->

      <div class="row middle">
        <div class="col"></div>
        <div class="col"></div>
        <div class="col"></div>
      </div> <!-- end-row -->

      <div class="row bottom">
        <div class="col"></div>
        <div class="col"></div>
        <div class="col"></div>
      </div> <!-- end-row -->
      
    </div> <!-- end-game -->
    
    <div class="scores">
      <div class="row">
        <div class="col p">
          <h2>Player - <span class="char">X</span></h2>
          <u>0</u>
        </div>
        <div class="col ties">
          <h2>Ties</h2>
          <u>0</u>
        </div>
        <div class="col com">
          <h2>Computer - <span class="char">O</span></h2>
          <u>0</u>
        </div>
      </div>
    </div>
    <!-- end-scores -->
    
  </div> <!-- end-container -->
  
</div> <!-- end-wrapper -->
@import "bourbon"
@import "neat"

// Config
$light: #FFFCCA
$color-1: #55E9BC
$color-2: #11D3BC
$dark: #537780

$base-size: 16px
$base-m: $base-size * 1.5

// Mixins
@mixin v-center()
  position: relative
  top: 50%
  +transform(translateY(-50%))
    
@mixin o()
  &:after
    content: ''
    border: solid $light
    border-radius: 50%
    border-width: rem(16)

    +media($bp-sm)
      border-width: rem(10)
  
    +media($bp-sm-h)
      border-width: rem(10)

    +size(60%)
    +position(absolute, 50% null null 50%)
    +transform(translateY(-50%) translateX(-50%))
    +animation(o .25s)
    
@mixin x()
  &:before        
    content: ''

    background-color: $light

    +size(10% 60%)
    +position(absolute, 50% null null 50%)
    +transform(translateY(-50%) translateX(-50%) rotate(45deg))
    +animation(x .25s)

  &:after
    content: ''

    background-color: $light

    +size(10% 60%)
    +position(absolute, 50% null null 50%)
    +transform(translateY(-50%) translateX(-50%) rotate(-45deg))
    +animation(x .25s)

// Typography
@import url(https://fonts.googleapis.com/css?family=Roboto)
@import url(https://fonts.googleapis.com/css?family=Montserrat)

// Base
body
  background-color: $color-1
  color: $dark
  
  font-size: $base-size
  font-family: 'Roboto', sans-serif
  
  text-align: center
  +user-select(none)

u
  text-decoration: none
  
.wrapper
  position: relative
  height: 100vh

// Grid
$fg-gutter: em(0)
$max-width: rem(480)
$grid-columns: 3

$bp-sm: new-breakpoint(max-width 520px)
$bp-sm-h: new-breakpoint(max-height 620px)
$bp-xs-h: new-breakpoint(max-height 420px)

.container
  +outer-container(rem(500))
    
  +media($bp-sm)
    +outer-container(rem(320))
  
  +media($bp-sm-h)
    +outer-container(rem(320))
  
  +media($bp-xs-h)
    +outer-container(rem(480))

.scores
  +span-columns(3)
  
  +media($bp-xs-h)
    +span-columns(1)
    +v-center
  
  .col
    +span-columns(1)
    
    +media($bp-xs-h)
      +span-columns(3)

.game
  +span-columns(3)
  
  +media($bp-xs-h)
    +span-columns(2)
    
  .row
    +row()

    .col
      +span-columns(1)
    
// Layout
.dialogs
  +position(absolute, 0 null null 0)
  z-index: 99
  
  +size(100%)
    
  .cover
    +position(absolute, 0 null null 0)
    +size(100%)
    background-color: $dark
    opacity: .8
  
  .pick,
  .end
    +v-center()
    padding: rem(15) 0
  
    .msg
      margin-top: 0
      margin-bottom: $base-m
      color: $light
      font-size: 3rem
      
      +media($bp-sm)
        font-size: 2rem
      
      +media($bp-sm-h)
        font-size: 2rem
      
    button
      position: relative
      border: none
      outline: none
      
      background-color: $color-2
      color: $light
      
      font-size: 6rem
      
      +transition(background-color .25s ease)
      
      +size(rem(160))
      margin-right: rem(5)
      
      &:last-child
        margin-right: 0
        
      &:hover
        background-color: shade($color-2, 10%)
      
      &:active
        background-color: $dark
        
      +media($bp-sm)
        +size(rem(100))
        font-size: 4rem
        
      +media($bp-sm-h)
        +size(rem(100))
        font-size: 4rem
      
      &.o
        +o()
        
      &.x
        +x()
        
  .end
    display: none
        
    
// end-dialgos

.container
  +v-center
  z-index: 1
  padding: 0 rem(10)
  
  +media($bp-xs-h)
    height: 100vh

// Game
.game
  min-width: rem(300)
  background-color: $color-2

  +media($bp-sm)
    max-width: rem(300)

  +media($bp-sm-h)
    max-width: rem(300)

  +media($bp-xs-h)
    +v-center

  .row

    &:last-child
      .col
        border-bottom: none

    .col
      +size(rem(160))
      position: relative

      cursor: pointer
      +transition(background-color .25s ease)

      border-bottom: rem(2) solid $color-1
      border-right: rem(2) solid $color-1

      color: $light
      font-size: 8rem

      +media($bp-sm)
        +size(rem(100))

      +media($bp-sm-h)
        +size(rem(100))

      +media($bp-xs-h)
        +size(rem(100))

      &:last-child
        border-right: none

      &:hover
        background-color: shade($color-2, 10%)

      &:active
        background-color: $dark
      
      +keyframes(blink)
        from
          background-color: $dark
        to
          background-color: transparent
      
      &.blink
        +animation(blink .25s 3)

      u
        +position(absolute, 0 null null 0)
        +size(100%)

        &.o
          +o()

          +keyframes(o)
            from
              +size(90%)
            to
              +size(60%)

        &.x
          +keyframes(x)
            from
              +size(15% 90%)
            to
              +size(10% 60%)

          +x()
// end-game

// Scores
.scores
  margin-top: $base-m
  cursor: pointer

  +media($bp-sm)
    max-width: rem(300)

  +media($bp-sm-h)
    max-width: rem(300)

  +media($bp-xs-h)
    max-width: rem(160)
    margin-top: 0
    +v-center

  .col
    +media($bp-xs-h)
      margin-bottom: $base-m

    &:last-child
      +media($bp-xs-h)
        margin-bottom: 0

  h2
    margin-top: 0
    margin-bottom: $base-m / 4

    +media($bp-sm)
      font-size: 1rem

    +media($bp-sm-h)
      font-size: 1rem

    .char
      font-family: 'Montserrat', sans-serif
      color: $light

  u
    font-family: 'Montserrat', sans-serif
    font-size: 3rem
    color: $light

    +media($bp-sm)
      font-size: 2rem

    +media($bp-sm-h)
      font-size: 2rem
// end-scores
          
View Compiled
'use strict';

(function() {
  
  function TicTacToe(args) {
    
    // Settings
    var $game = args.game,
        $scores = args.scores,
        $dialogs = args.dialogs,
        cols = [];
    
    $game.find('.row').each(function(i) {
      var row = [];
      $(this).find('.col').each(function(j) {
        row.push($(this));
      });
      cols.push(row);
    });
    
    // VARS
    var rows = [
          [cols[0][0], cols[0][1], cols[0][2]],
          [cols[1][0], cols[1][1], cols[1][2]],
          [cols[2][0], cols[2][1], cols[2][2]], // Hori

          [cols[0][0], cols[1][0], cols[2][0]],
          [cols[0][1], cols[1][1], cols[2][1]],
          [cols[0][2], cols[1][2], cols[2][2]], // Verti

          [cols[0][0], cols[1][1], cols[2][2]],
          [cols[0][2], cols[1][1], cols[2][0]] // Diago
        ],
        chars = { p: 'x', com: 'o' },
        scores = {  p: 0, ties: 0, com: 0 },
        turn = 'p',
        isComputer = false;
    
    /*
    ============================================
      UpdateScores Function.
    ============================================
    */
    function updateScores() {
      
      $scores.find('.p').find('u').html(scores.p);
      $scores.find('.ties').find('u').html(scores.ties);
      $scores.find('.com').find('u').html(scores.com);
      
    } // end-updateScores
    
    /*
    ============================================
      getCoords Function.
    ============================================
    */
    function getCoords(target) {
        
      for (var i = 0; i < cols.length; i++) {
        for (var j = 0; j < cols[i].length; j++) {
          if (target.context === cols[i][j].context) {
            return { row: i, col: j };
          }
        }
      }

    } // end-getCoords
    
    /*
    ============================================
      AppendChar Function.
    ============================================
    */
    function appendChar(target, char) {
      
      if (target.hasClass('col') && target.children().length < 1) {
        target.append($(document.createElement('u')).addClass(char));
      }
      
    } // end-appendChar
    
    /*
    ============================================
      Blink Function.
    ============================================
    */
    function blink($el) {
      
      function rmClass() {
        $el.removeClass('blink');
      }
      
      $el.addClass('blink');
      setTimeout(rmClass, 2000);
      
    } // end-blink
    
    /*
    ============================================
      SwitchTurn Function.
    ============================================
    */
    function switchTurn() {
      
      if (turn === 'p') {
        turn = 'com';
      } else {
        turn = 'p';
      }
      
    } // end-switchTurn
    
    /*
    ============================================
      Dialogs Function.
    ============================================
    */
    function dialogs(fade, dialog) {
      
      if (fade === 'out') {
        $dialogs.fadeOut(500, function() {
          $dialogs.find('.end').find('.msg').html('');
        });
      } else {
        $dialogs.children().show();
        $dialogs.find('.' + dialog).hide(0, function() {
          $dialogs.fadeIn(500);
        });
      }

    } // end-dialogs
    
    /*
    ============================================
      Action Function.
    ============================================
    */
    function action(action) {
      
      cols.forEach(function(row, i) {
        row.forEach(function(col, i) {
          
          if (action === 'replay') {
            col.children('u').remove();
          } else if (action === 'tie') {
            blink(col);
          }
          
        });
      });
      
      if (action === 'replay') {
        dialogs('out', 'pick');
        switchTurn();
        if (turn === 'com') {
          setTimeout(computer, 500);
        }
      } else if (action === 'win') {
        dialogs('in', 'pick');
      } else if (action === 'tie') {
        $dialogs.find('.msg').html('Tie');
        dialogs('in', 'pick');
      }
      
    } // end-action
    
    /*
    ============================================
      Winner Function.
    ============================================
    */
    function checkWinner() {
      
      function getRow(char) {
        
        rowsLoop:
        for (var i = 0; i < rows.length; i++) {
          for (var j = 0; j < rows[i].length; j++) {
            if (!rows[i][j].children('u').first().hasClass(char)) {
              continue rowsLoop;
            }
          }
          return rows[i];
        }
        
      } // end-getRow
            
      var p = getRow(chars.p),
          com = getRow(chars.com);
      
      if (p) {
        return {
          name: 'p',
          row: p
        };
      } else if (com) {
        return {
          name: 'com',
          row: com
        };
      }
      return false;
      
    } // end-checkWin
    
    function win(winner) {
      
      function winAction(row, text) {
        row.forEach(function(col) {
          blink(col);
        });
        $dialogs.find('.msg').html(text);
        action('win');
      } // action
      
      if (winner.name === 'p') {
        winAction(winner.row, 'You win!!');
        scores.p++
        updateScores();
      } else if (winner.name === 'com') {
        winAction(winner.row, 'Computer wins!');
        scores.com++
        updateScores();
      }

    } // end-win
    
    /*
    ============================================
      Tie Function.
    ============================================
    */
    function checkTie() {
      
      var emptyFound = false;
      colsLoop:
      for (var i = 0; i < cols.length; i++) {
        for (var j = 0; j < cols[i].length; j++) {
          if (!cols[i][j].children('u').length) {
            emptyFound = true;
            break colsLoop;
          }
        }
      }
      if (emptyFound) {
        return false;
      }
      return true;
      
    } // end-checkTie
    
    function tie() {
      
      action('tie');
      scores.ties++
      updateScores();
      
    } // end-tie
    
    /*
    ============================================
      Computer Function.
    ============================================
    */
    function computer() {
      
      if (checkWinner()) {
        isComputer = false;
        var winner = checkWinner();
        win(winner);
        return;
      } else if (checkTie()) {
        isComputer = false;
        tie();
        return;
      }
      
      isComputer = true;
      
      function getRandom(arr) {
        var randomIndex = parseInt(Math.random() * arr.length);
        return arr[randomIndex];
      }
      
      function getRows(char) {
        
        var dirtyRows = [];
        
        for (var i = 0; i < rows.length; i++) {
          var dirtyRowData = [];
          for (var j = 0; j < rows[i].length; j++) {
            if (rows[i][j].children('u').hasClass(char)) {
              dirtyRowData.push(rows[i][j]);
            }
          }
          if (dirtyRowData.length) {
            dirtyRowData.push(rows[i]);
            dirtyRows.push(dirtyRowData);
          }
        }
        
        var hasEmptyCols = [];
        
        dirtyRows.forEach(function(arr, i) {
          var row = arr[arr.length - 1];
          for (var i = 0; i < row.length; i++) {
            if (!row[i].children('u').length) {
              hasEmptyCols.push(arr);
            }
          }
        });
        
        var hasEmptyCols = hasEmptyCols.filter(function(row, i) {
          return hasEmptyCols.indexOf(row) === i;
        });
        
        hasEmptyCols.sort(function(a, b) {
          return b.length - a.length;
        });
                
        var hasHigherLength = hasEmptyCols.filter(function(row) {
          return row.length === hasEmptyCols[0].length;
        });
        
        var collection = [];
        hasHigherLength.forEach(function(row) {
          collection.push(row[row.length - 1]);
        });
        
        return collection ? collection : false;
        
      } // end-getRows
      
      function getCol() {
        
        function getEmptyCols(row) {
          var cols;
          if (row) {
            cols = row.filter(function(col) {
              return col.children('u').length === 0;
            });
          } else {
            cols = false;
          }
          return cols;
        } // end-getEmptyCols
        
        function checkWinnerRows(rows, char) {
          if (!rows) return false;
          var winnerRows = [];
          rows.forEach(function(row) {
            var dirtyCols = [];
            for (var i = 0; i < row.length; i++) {
              if (row[i].children('u').hasClass(char)) {
                dirtyCols.push(row[i]);
              }
            }
            if (dirtyCols.length === 2) {
              winnerRows.push(row);
            }
          });
          if (winnerRows.length) {
            return winnerRows;
          }
          return false;
        } // end-checkWinnerRows
        
        function getTheLastEmptyCol() {
          
          var col;
          rows.forEach(function(row) {
            for (var i = 0; i < row.length; i++) {

              if (!row[i].children('u').length) {
                col = row[i];
              }

            }
          });
          return col;

        } // end-getTheLastEmptyCol
        
        var cRows = getRows(chars.com),
            pRows = getRows(chars.p),
            cWinnerRows = checkWinnerRows(cRows, chars.com),
            pWinnerRows = checkWinnerRows(pRows, chars.p),
            randomRow = getRandom(rows);
        
        if (cWinnerRows.length) {
          return getRandom(getEmptyCols(getRandom(cWinnerRows)));
        } else if (pWinnerRows.length) {
          return getRandom(getEmptyCols(getRandom(pWinnerRows)));
        } else if (pRows.length) {
          return getRandom(getEmptyCols(getRandom(pRows)));
        } else if (cRows.length) {
          return getRandom(getEmptyCols(getRandom(cRows)));
        } else if (randomRow.length) {
          return getRandom(getEmptyCols(randomRow));
        } else {
          return getTheLastEmptyCol();
        }
        
      } // end-getCol
      
      var col = getCol();
      
      appendChar(col, chars.com);
      
      isComputer = false;
      
      if (checkWinner()) {
        var winner = checkWinner();
        win(winner);
        return;
      } else if (checkTie()) {
        tie();
        return;
      }
      
    } // end-computer
    
    
    /*
    ============================================
      Player Function.
    ============================================
    */
    function player(target) {
      
      if (isComputer || !target.hasClass('col') || target.children('u').length) {
        return;
      }
      
      var coords = getCoords(target);
      
      appendChar(cols[coords.row][coords.col], chars.p);
      
      if (checkWinner()) {
        var winner = checkWinner();
        win(winner);
        return;
      } else if (checkTie()) {
        tie();
        return;
      }
      
      isComputer = true;
      setTimeout(computer, 250);
        
    } // end-player
    
    /*
    ============================================
      Events Function.
    ============================================
    */
    $game.on('click', function(e) {
      
      var target = $(e.target);
      
      player(target);
              
    });
    
    $dialogs.find('.pick').find('button').on('click', function(e) {
      
      var target = $(e.target);      
      if (target.hasClass('x')) {
        chars.p = 'x';
        chars.com = 'o';
        $scores.find('.p').find('.char').html('X');
        $scores.find('.com').find('.char').html('O');
      } else {
        chars.p = 'o';
        chars.com = 'x';
        $scores.find('.p').find('.char').html('O');
        $scores.find('.com').find('.char').html('X');
      }
      dialogs('out', 'pick');
      
    });
    
    $dialogs.find('.end').find('.replay').on('click', function(e) {
      
      action('replay');
      
    });
    
  } // end-TicTacToe
  
  $(document).ready(function() {
    
    // DOM
    var $game = $('.game'),
        $scores = $('.scores'),
        $dialogs = $('.dialogs');
    
    var game = new TicTacToe({
      
      game: $game,
      scores: $scores,
      dialogs: $dialogs
      
    });
    
  });
  
})()
Run Pen

External CSS

  1. //cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js