<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">

<div class="drawer">

    <header>
      <button class="play-btn"></button>
      <p class="message"></p>

      <div class="dice-roll">
        <div class="opponent"></div>
        <div class="dice-rolling"></div>
        <div class="dice-score"></div>
        <div class="dice-result"></div>
      </div>

    </header>

    <main class="board">
      <div class="cell circle"></div>
      <div class="cell cross"></div>
      <div class="cell circle"></div>
      <div class="cell cross"></div>
      <div class="cell circle"></div>
      <div class="cell"></div>
      <div class="cell cross"></div>
      <div class="cell cross"></div>
      <div class="cell"></div>
    </main>

    <footer class="scores hide">

      <div>
        <span></span>
        <ul>
          <li></li>
          <li></li>
          <li></li>
        </ul>
      </div>

      <div>
        <span></span>
        <ul>
          <li></li>
          <li></li>
          <li></li>
        </ul>
      </div>

    </footer>

  </div>
*, *::after, *::before {
      box-sizing: border-box;
    }

html {
  font-size:62.5%;
}

body {
  font-size:1.6rem;
  margin:0;
  height:100vh;
  background:hsl(300, 15%, 36%);
  font-family: 'Montserrat', 'Arial', sans-serif;
  letter-spacing: 1px;
}

.drawer {
  width: 80%;
  margin:0 auto;
  padding-top:60px;
}

.board {
  display:flex;
  flex-wrap: wrap;
  width: 320px;
  height: 320px;
  margin: 0 auto;
}

.cell {
  position: relative;
  width:90px;
  height:90px;
  margin:5px;
  border-radius: 0.3em;
  background:hsl(300, 15%, 33%);
}

.cell.circle,
.cell.cross {
  background:transparent;
}

.circle::after,
.cross::before,
.cross::after {
  content:'';
  position:absolute;
  top:50%;
  left:50%;
}

.cross::before,
.cross::after {
  width:5px;
  height:75px;
  background:hsl(300, 15%, 33%);
}

.playing .cross::before,
.playing .cross::after {
  background:hsl(194, 100%, 73%);
}

.cross::before {
  transform:translate(-50%, -50%) rotate(45deg);
}

.cross::after {
  transform:translate(-50%, -50%) rotate(-45deg);
}

.circle::after {
  width:70px;
  height:70px;
  border-radius:50%;
  transform:translate(-50%, -50%);
  border:5px solid hsl(300, 15%, 33%);
}

.playing .circle::after {
  border-color:hsl(7, 63%, 78%);
}

.playing .cell:not(.cross):not(.circle){
  cursor:pointer;
}

.playing .cell:not(.cross):not(.circle):hover{
  background:hsl(300, 15%, 34%);
}

#instructions {
  display: none;
}

.message {
  text-align: center;
  color: hsla(300, 15%, 20%, 1);
  font-size: 2rem;
}

.play-btn {

  position:absolute;
  top:0;
  left:50%;
  outline:none;
  border:none;
  cursor:pointer;

  background: hsl(300, 3%, 18%);
  padding: 1rem 1.5rem;

  font-size: 2.4rem;
  color:hsla(300, 15%, 44%, 1);
  border-radius: 0 0 0.2rem 0.2rem;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  border:1px solid hsla(300, 3%, 17%, 1);
  transform:translate(-50%, 0);
  transition: transform 200ms ease-out;
}

.play-btn:hover {
  background: hsl(300, 3%, 20%);
}

.play-btn.hide {
  display:inline-block;
  transform:translate(-50%, -100%);
}

header {
  max-width: 320px;
  margin:0 auto 20px;
}

.scores {
  display:flex;
  justify-content: space-between;
  position:relative;
  max-width: 320px;
  margin:2rem auto 0 ;
  border-top: 2px solid hsla(300, 15%, 20%, 1);
  padding-top: 2rem;
  opacity: 1;
  transform: translate(0, 0);
  transition: all 200ms 75ms ease-out;
}

.scores.hide {
  display: flex;
  opacity:0;
  transform: translate(0, 20%);
}

.scores div {
  flex:1;
}

.scores span {
  display:block;
  color:hsla(300, 15%, 20%, 1);
}


.scores ul {
  list-style: none;
  margin:0;
  padding: 0;
  display: inline-block;
}

.scores li {
  width: 10px;
  height:10px;
  border: 2px solid hsla(300, 15%, 20%, 1);
  border-radius:50%;
  display: inline-block;
}

.scores li.won {
  background: hsla(300, 15%, 53%, 1);
  animation: win 300ms;
}

@keyframes win {
  0% {transform: scale(1);}
  40% {transform: scale(3); }
  100% {transform: scale(1);}
}

.scores::after{
  display: none;
  content: 'vs';
  position: absolute;
  left:50%;
  top:50%;
  font-size: 2.4rem;
  transform:translate(-50%, -50%);
  color:#bdbdbd;
}

.scores >div:last-child{
  text-align: right;
}


.hide {  display:none;}

.dice-roll {
  text-align: center;
  padding:1.5rem;
  font-size:1.4rem;
}
View Compiled
const EMPTY = -1;
const PLAYER = 0;
const OPPONENT = 1;

// DOM
const $board = document.querySelector('.board');
const $cells = Array.from($board.children);
const $diceRoll = document.querySelector('.dice-roll');
const $scores = document.querySelector('.scores');
const $message = document.querySelector('.message');
const $playBtn = document.querySelector('.play-btn');

let board = emptyBoard();
let winPatterns = [
    0b111000000, 0b000111000, 0b000000111, // rows
    0b100100100, 0b010010010, 0b001001001, // cols
    0b100010001, 0b001010100 // diags
];


// Minimax (see http://www.geeksforgeeks.org/minimax-algorithm-in-game-theory-set-1-introduction/)
class AI {

    constructor(difficulty = 1) {
        this.difficulty = difficulty;
    }

    findBestMove() {
        return this.minimax(this.difficulty, OPPONENT).position;
    }

    minimax(depth, minmaxer) {

        let nextMoves = getAvailableMoves();
        let bestMove = { score: (minmaxer === OPPONENT) ? -10000 : 10000,  position: -1};

        // Collect every available move
        let randomizedMoves = [];

        if (!nextMoves.length || depth === 0) {
            bestMove.score = this.evaluate();
        } else {

            for (let i = 0; i < nextMoves.length; ++i) {

                let moveSimulation = nextMoves[i];
                board[moveSimulation] = minmaxer;

                let score = this.minimax(depth - 1, (minmaxer === OPPONENT) ? PLAYER : OPPONENT).score;

                randomizedMoves.push({score:score, position:moveSimulation});

                if ((minmaxer === OPPONENT && score > bestMove.score) ||
                    (minmaxer === PLAYER && score < bestMove.score)) {
                    bestMove = {score: score, position: moveSimulation };
                }

                board[moveSimulation] = EMPTY;
            }
        }
        
        // Take one random move if several moves with the same score are available. 
        if(randomizedMoves.length){
            
            // First AI move
            if(randomizedMoves.length === board.length){
                bestMove = randomizedMoves[Math.floor(Math.random() * randomizedMoves.length)];
            } else {
                randomizedMoves = randomizedMoves.filter( m => m.score === bestMove.score);
                bestMove = randomizedMoves[Math.floor(Math.random() * randomizedMoves.length)];
            }
        }

        return bestMove;
    }

    // Score Heuristic Evaluation
    evaluate() {

        let score = 0;

        score += this.evaluateLine(0, 1, 2); // row 1
        score += this.evaluateLine(3, 4, 5); // row 2
        score += this.evaluateLine(6, 7, 8); // row 3
        score += this.evaluateLine(0, 3, 6); // col 1
        score += this.evaluateLine(1, 4, 7); // col 2
        score += this.evaluateLine(2, 5, 8); // col 3
        score += this.evaluateLine(0, 4, 8); // diag.
        score += this.evaluateLine(2, 4, 6); // alt. diag.

        return score;
    }

    evaluateLine(a, b, c) {

        let score = 0;
        let cA = board[a];
        let cB = board[b];
        let cC = board[c];

        // first cell
        if (cA == OPPONENT) {
            score = 1;
        } else if (cA == PLAYER) {
            score = -1;
        }

        // second cell
        if (cB == OPPONENT) {
            if (score == 1) {
                score = 10;
            } else if (score == -1) {
                return 0;
            } else {
                score = 1;
            }
        } else if (cB == PLAYER) {
            if (score == -1) {
                score = -10;
            } else if (score == 1) {
                return 0;
            } else {
                score = -1;
            }
        }

        // third cell
        if (cC == OPPONENT) {
            if (score > 0) {
                score *= 10;
            } else if (score < 0) {
                return 0;
            } else {
                score = 1;
            }
        } else if (cC == PLAYER) {
            if (score < 0) {
                score *= 10;
            } else if (score > 1) {
                return 0;
            } else {
                score = -1;
            }
        }

        return score;
    }

}

class HumanPlayer {

    constructor() {
        this.name = 'You';
        this.win = 0;
    }

    play() {
        $message.textContent = 'Your turn!';

        return new Promise((resolve) => {
            let disposeFn = event($board, 'click', e => {
                let target = e.target;
                if (target.classList.contains('cell')) { // If we hit a cell
                    let idx = $cells.indexOf(target); // get the cell index.
                    if (getAvailableMoves().indexOf(idx) !== -1) { // must be available
                        disposeFn();
                        resolve(idx);
                    }
                }
            });
        })
    }

}

class AIPlayer {

    constructor(difficulty = 2) {
        this.difficulty = difficulty;
        this.name = `${this._getRandomName()}(AI)`;
        this.win = 0;
    }

    _getRandomName() {
        return AIPlayer.names[Math.floor(Math.random() * (AIPlayer.names.length - 1))];
    }

    setBoard() {
        this.ai = new AI(1);
    }

    play() {
        $message.textContent = `${this.name}'s turn`;
        return new Promise((res) => {
            let randomTimer = Math.floor(Math.random() * 1000 + 500);
            let move = this.ai.findBestMove();
            setTimeout(() => res(move), randomTimer);
        })
    }
}

AIPlayer.names = ['Leanne', 'Ervin', 'Clementine', 'Patricia', 'Chelsey', 'Dennis', 'Kurtis',
    'Nicholas', 'Alphonse', 'Marie', 'Edouard', 'Lucille', 'Julie', 'Bernard'
];

let player = null;
let opponent = null;
let startingPlayer = null;
let currentPlayer = null;

/**
 * Game utils
 */
function emptyBoard() {
    return [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY];
}

function hasAvailableMove() {
    return board.some(cell => cell === EMPTY);
}

function getAvailableMoves() {
    return board.reduce((acc, current, idx) => {
        current === EMPTY && acc.push(idx);
        return acc;
    }, []);
}

function hasWon(player) {

    let pattern = board.reduce((acc, curr, i) => {
        curr === player.symbol && (acc |= (1 << i));
        return acc;
    }, 0b000000000);

    return winPatterns.some(winPattern => {
        return (pattern & winPattern) == winPattern;
    });
}

function getWinner() {
    if (hasWon(player)) return player;
    if (hasWon(opponent)) return opponent;
    return null;
}

function clearBoard() {
    board = emptyBoard();
    $cells.forEach(cell => {
        cell.classList.remove('cross');
        cell.classList.remove('circle');
    });
}

function updateBoard(idx, symbol) {
    board[idx] = symbol;
    $board.children[idx].classList.add(symbol === PLAYER ? 'cross' : 'circle');
}

function isOver() {
    return hasWon(player) || hasWon(opponent) || !hasAvailableMove();
}

function declareTurnWinner() {

    let winner = getWinner();

    if (winner) {

        winner.win++;
        $message.textContent = `${winner.name} win!`;
        $scores.children[winner.symbol].querySelectorAll('li')[winner.win - 1].classList.add('won');

        if (player.win == 3) {
            endState(player);
        } else if (opponent.win == 3) {
            endState(opponent);
        } else {
            nextTurn();
        }

    } else {
        $message.textContent = `Draw!`;
        nextTurn();
    }
}

function nextTurn() {
    $playBtn.textContent = 'Next turn';
    $playBtn.classList.remove('hide');

    let disposeEvent = event($playBtn, 'click', () => {
        currentPlayer = startingPlayer;
        $playBtn.classList.add('hide');
        clearBoard();
        disposeEvent();
        takeTurn();
    });
}

function getOpponent(which) {
    return which === player ? opponent : player;
}

function takeTurn() {
    return currentPlayer.play()
        .then(move => {
            updateBoard(move, currentPlayer.symbol);
            currentPlayer = getOpponent(currentPlayer);
            return isOver() ? declareTurnWinner() : takeTurn();
        })
}

/**
 * Events handling
 */
let events = [];

function event(target, type, handler) {
    target.addEventListener(type, handler);
    return function disposeEvent() {
        target.removeEventListener(type, handler);
    }
}

function removeEvents() {
    events.forEach(disposeFn => disposeFn());
    events = [];
}

/**
 * Game States
 */
function initState() {

    removeEvents();

    $scores.classList.add('hide');
    $diceRoll.classList.add('hide');
    $playBtn.classList.remove('hide');

    $playBtn.textContent = 'Click to start';
    $message.textContent = 'Tic Tac Toe';

    events.push(event($playBtn, 'click', playerSetup));
}

function dice() {

    $playBtn.classList.add('hide');
    document.body.classList.remove('playing');

    setTimeout(() => {
        $playBtn.textContent = 'Click to throw the dice';
        $playBtn.classList.remove('hide');
    }, 500);

    let disposeEvent = event($playBtn, 'click', onDiceRoll);

    function onDiceRoll() {

        $playBtn.classList.add('hide');

        $diceRoll.querySelector('.dice-rolling').textContent = 'The dices are rolling!';

        let scoreA = Math.floor(Math.random() * 5) + 1;
        let scoreB = Math.floor(Math.random() * 3) + 1; // Yes...cheating here, so player has more chance to start... :) 

        while (scoreA === scoreB) {
            scoreA = Math.floor(Math.random() * 5) + 1;
            scoreB = Math.floor(Math.random() * 3) + 1; 
        }

        startingPlayer = scoreA > scoreB ? player : opponent;
        currentPlayer = startingPlayer;

        disposeEvent();

        setTimeout(() => {

            $diceRoll.querySelector('.dice-score').textContent = `You: ${scoreA} - ${opponent.name}: ${scoreB}.`;
            $diceRoll.querySelector('.dice-result').textContent = `${startingPlayer.name} start!`;

            $playBtn.textContent = 'Start';
            $playBtn.classList.remove('hide');

            events.push(event($playBtn, 'click', playingState));
        }, 1000);
    }

}

function playerSetup() {

    removeEvents();

    $scores.classList.add('hide');
    $message.classList.add('hide');
    $playBtn.classList.add('hide');
    $board.classList.add('hide');
    $diceRoll.classList.remove('hide');

    $diceRoll.querySelector('.dice-rolling').textContent = '';
    $diceRoll.querySelector('.dice-score').textContent = '';
    $diceRoll.querySelector('.dice-result').textContent = '';

    player = new HumanPlayer();
    player.symbol = PLAYER;

    opponent = new AIPlayer();
    opponent.symbol = OPPONENT;
    opponent.setBoard(board);

    $diceRoll.querySelector('.opponent').textContent = `You are playing against ${opponent.name}`;

    dice();
}

function playingState() {

    removeEvents();
    clearBoard();
    Array.from($scores.querySelectorAll('li')).forEach(li => li.classList.remove('won'));

    $board.classList.remove('hide');
    $scores.classList.remove('hide');
    $playBtn.classList.add('hide');
    $diceRoll.classList.add('hide');
    $message.classList.remove('hide');

    $scores.children[PLAYER].querySelector('span').textContent = player.name;
    $scores.children[OPPONENT].querySelector('span').textContent = opponent.name;

    document.body.classList.add('playing');

    takeTurn();
}

function endState(winner) {
    removeEvents();

    $message.textContent = `${winner.name} wins the game!`;
    document.body.classList.remove('playing');

    $playBtn.classList.remove('hide');
    $playBtn.textContent = 'Try again!';

    events.push(event($playBtn, 'click', playerSetup));
}

initState();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.