<div id="tic_tac_toe">
  <div class="tiles">
    <button v-for="button, index in buttons"
            v-bind:class="{
                            'tile' : true,
                            'active' : button.active,
                            'match': button.match,
                            'disabled' : !play
                          }"
            v-bind:data-tile="index"
            v-on:click.self="tileClick">
      <div class="turn turn-0"
           v-if="button.player === 0"></div>
      <svg viewBox="0 0 30 30"
           class="turn turn-1"
           v-if="button.player === 1">
        <circle fill="transparent"
                stroke="#d10827"
                stroke-width="3"
                r="10" cx="15" cy="15" />
      </svg>
    </button>
  </div>
  <div class="modal-window"
       v-bind:class="{ 'shown' : showDialog }">
    <!--STEP 1-->
    <div class="dialog-box" v-if="dialogStep === 0">
      <div class="dialog-content">
        <p class="text-bold">{{ GameTitle }}</p>
        <p>Welcome to the example TicTacToe game I made.</p>
        <p>You may play this with your friend, or versus the computer.</p>
      </div>
      <div class="dialog-content">
        <button class="btn tile"
                v-on:click="proceedToNext()">
          Proceed to Game</button>
      </div>
    </div>
    <!--STEP 2-->
    <div class="dialog-box" v-if="dialogStep === 1">
      <div class="dialog-content">
        <p class="text-bold">{{ GameTitle }}</p>
        <p>Choose your opponent to play with.</p>
      </div>
      <div class="dialog-content">
        <button class="btn tile"
                v-on:click="chooseOpponent(false)">
          Friend</button>
        <button class="btn tile"
                v-on:click="chooseOpponent(true)">
          Computer</button>
      </div>
    </div>
    <!--STEP 3-->
    <div class="dialog-box" v-if="dialogStep === 2">
      <div class="dialog-content">
        <p class="text-bold">{{ GameTitle }}</p>
        <p>Choose your player.</p>
      </div>
      <div class="dialog-content">
        <button class="btn btn-turn tile"
                v-on:click="choosePlayer(0)">
          <div class="turn turn-0"></div>
        </button>
        <button class="btn btn-turn tile"
                v-on:click="choosePlayer(1)">
          <svg viewBox="0 0 30 30"
           class="turn turn-1">
            <circle fill="transparent"
                    stroke="#d10827"
                    stroke-width="3"
                    r="10" cx="15" cy="15" />
          </svg>
        </button>
      </div>
    </div>
    <!--STEP 4-->
    <div class="dialog-box" v-if="dialogStep === 3">
      <div class="dialog-content">
        <p class="text-bold">{{ GameTitle }}</p>
        <p>Choose the computer's difficulty.</p>
      </div>
      <div class="dialog-content">
      </div>
      <div class="dialog-content">
        <button class="btn tile"
                v-on:click="chooseDifficulty(0)">
          Easy</button>
        <button class="btn tile"
                v-on:click="chooseDifficulty(1)">
          Medium</button>
        <button class="btn tile"
                v-on:click="chooseDifficulty(2)">
          Hard</button>
      </div>
    </div>
    <!--STEP 5/GAME END-->
    <div class="dialog-box" v-if="dialogStep === 4">
      <div class="dialog-content">
        <p class="text-bold">Game ended!</p>
        <p v-if="winningPlayer !== null">
          Player '{{ winningPlayer }}' won the game! Would you like to play again?
        </p>
        <p v-else="">
          The match is a draw! Would you like to play again?
        </p>
      </div>
      <div class="dialog-content">
        <button class="btn tile"
                v-on:click="initialize()">
          Start New</button>
        <button class="btn tile"
                v-on:click="playAgain()">
          Play Again</button>
      </div>
    </div>
  </div>
</div>
@import url('https://fonts.googleapis.com/css?family=Comfortaa&display=swap');

* {
  font-family: 'Comfortaa', cursive;
}

#tic_tac_toe {
  background-image: linear-gradient(#1fa5ff 0% 30%, #57f9ff 70% 100%);
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tiles {
  --cell-size: 15vmin;
  --shadow-opacity: 0.4;
  background-color: rgba(#000000, 0.15);
  padding: 10vmin;
  border-radius: 5vmin;
  display: grid;
  grid-template-rows: repeat(3, var(--cell-size));
  grid-template-columns: repeat(3, var(--cell-size));
  grid-gap: 1.75vmin;
  box-shadow: 0 1.5vmin rgba(#000000, var(--shadow-opacity));
}

.tile {
  --shadow-size: 0.75vmin;
  --transition-time: 150ms;
  background-color: #ffc417;
  background-image: linear-gradient(to bottom right, rgba(#ffffff, 0.4) 0 30%, rgba(#ffffff, 0) 70% 100%);
  min-width: 0;
  min-height: 0;
  padding: 0;
  margin: 0;
  border: none;
  border-radius: 2vmin;
  outline: none;
  box-shadow:
    0 var(--shadow-size) rgba(#000000, var(--shadow-opacity)),
    0 0 0 rgba(#000000, var(--shadow-opacity)) inset;
  transition:
    background-color var(--transition-time) ease-out,
    box-shadow var(--transition-time) ease-out,
    transform var(--transition-time) ease-out;
  &:active:not(.disabled), &.active, &.disabled.active {
    box-shadow:
      0 0 rgba(#000000, var(--shadow-opacity)),
      0 var(--shadow-size) calc(var(--shadow-size) * 2) rgba(#000000, var(--shadow-opacity)) inset;
    transform: translateY(var(--shadow-size));
  }
  &.disabled {
    background-color: #9f9f9f;
  }
  &.active {
    background-color: #e2e2e2;
  }
  &.match {
    background-color: #ffe11f;
  }
}

.turn {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  vertical-align: top;
}

.turn-0 {
  --draw-time: 250ms;
  position: relative;
  &:before, &:after {
    content: '';
    background-color: #0049bf;
    width: 12%;
    height: 0;
    position: absolute;
    top: 22%;
    transform-origin: top center;
  }
  &:before {
    left: 15.5%;
    transform: rotate(-45deg);
    animation: turn-0 var(--draw-time) ease-out forwards;
  }
  &:after {
    right: 15.5%;
    transform: rotate(45deg);
    animation: turn-0 var(--draw-time) ease-out var(--draw-time) forwards;
  }
}

@keyframes turn-0 {
  0%   { height: 0; }
  100% { height: 80%; }
}

.turn-1 {
  // circumference = 2 * PI * 10 (rounded to 10 thousandths)
  transform: rotate(-90deg);
  animation: turn-1 500ms ease-out forwards;
}

@keyframes turn-1 {
  0%   { stroke-dasharray: 0 62.8319; }
  100% { stroke-dasharray: 62.8319 62.8319; }
}

.modal-window {
  width: 100vw;
  height: 100vh;
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: scale(0);
  transition: transform 250ms ease-out;
  &.shown {
    transform: scale(1);
  }
}

.dialog-box {
  font-size: 16px;
  text-align: center;
  background-image: linear-gradient(
    to bottom right,
    rgba(#dfdfdf, 0.90) 0% 30%,
    rgba(#ffffff, 0.90) 70% 100%);
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  p {
    padding: 0;
    margin: 0 0 0.5em 0;
    &:last-child {
      margin: 0;
    }
  }
}

.dialog-content {
  padding: 0.5em 0;
}

.btn {
  --shadow-size: 0.75vmin;
  --shadow-opacity: 0.5;
  padding: 0.5em 0.8em;
  &:active {
    --shadow-opacity: 0.25;
  }
}

.btn-turn {
  width: 3em;
  height: 3em;
  padding: 0;
  vertical-align: middle;
}

.text-bold {
  font-weight: bold;
}
View Compiled
enum Player {
  X = 0,
  O = 1
}

enum Difficulty {
  Easy = 0,
  Medium = 1,
  Hard = 2
}

enum Matches {
  Two = 2,
  One = 4
}

let app: object = new Vue({
  el: "#tic_tac_toe",
  data: {
    buttons: [],
    combinations: [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6]
    ],
    GameTitle: "Tic Tac Toe V.3",
    aiPlayer: null,
    aiDifficulty: null,
    turn: 0,
    play: true,
    match: null,
    showDialog: true,
    winningPlayer: null,
    dialogStep: 0
  },
  methods: {
    // Analyze the provided combination if the player gets
    // 3 consecutives. For AI purposes, +2 of each was added
    // if has 2 combinations. +4 if has 1 combination.
    analyzeCombination(open: array): number {
      open.sort();

      let countX: number = 0;
      let countO: number = 0;

      for (let i: number in open) {
        if (open[i] === Player.X) {
          countX++;
        } else if (open[i] === Player.O) {
          countO++;
        }
      }

      if (countX === 3) {
        return Player.X;
      } else if (countO === 3) {
        return Player.O;
      } else if (countX === 2) {
        return Player.X + Matches.Two;
      } else if (countO === 2) {
        return Player.O + Matches.Two;
      } else if (countX === 1) {
        return Player.X + Matches.One;
      } else if (countO === 1) {
        return Player.O + Matches.One;
      } else {
        return null;
      }
    },
    createOpened(combination: number): array {
      let open: array = [];

      for (let i: number = 0; i < combination.length; i++) {
        let index: number = combination[i];

        open.push(this.buttons[index].player);
      }

      return open;
    },
    // Find the combinations of the opened tiles
    findCombination(): boolean {
      for (let i: number = 0; i < this.combinations.length; i++) {
        let combination: array = this.combinations[i];
        let open: array = this.createOpened(combination);
        let analysis: number = this.analyzeCombination(open);

        if (analysis === Player.X || analysis === Player.O) {
          this.play = false;

          this.match = combination;

          return true;
        }
      }

      return false;
    },
    // Get the buttons that aren't yet opened
    getFreeButtons(): array {
      let freeButtons: array = [];

      for (let i: number = 0; i < this.buttons.length; i++) {
        if (this.buttons[i].active === false) {
          freeButtons.push(i);
        }
      }

      return freeButtons;
    },
    // Insert the possible turn of AI
    robotInsertTurn(combination: array): number {
      let turn: number = null;

      for (let j: number = 0; j < combination.length; j++) {
        if (this.buttons[combination[j]].player === null) {
          turn = combination[j];
          break;
        }
      }

      return turn;
    },
    // Random move by AI
    robotRandom(): number {
      let freeButtons: array = this.getFreeButtons();
      let random: number = Math.round(Math.random() * (freeButtons.length - 1));

      if (random >= 0) {
        return freeButtons[random];
      } else {
        return null;
      }
    },
    // Find combination for AI move
    robotCombinaton(selectedMatches: number): number {
      let turn: number = null;

      for (let i: number = 0; i < this.combinations.length; i++) {
        let combination: array = this.combinations[i];
        let open: array = this.createOpened(combination);
        let analysis: number = this.analyzeCombination(open);

        // First priority is 2 matches, then 1
        if (analysis === selectedMatches) {
          turn = this.robotInsertTurn(combination);

          break;
        }
      }

      return turn;
    },
    // Attack move by AI
    robotAttack(tileIndex: number): number {
      if (tileIndex === null) {
        tileIndex = this.robotCombinaton(this.aiPlayer + Matches.Two);
      }

      if (tileIndex === null) {
        tileIndex = this.robotCombinaton(this.aiPlayer + Matches.One);
      }

      return tileIndex;
    },
    // Defend move by AI
    robotDefend(): number {
      let tileIndex = null;
      let human = this.aiPlayer === 0 ? 1 : 0;

      tileIndex = this.robotCombinaton(human + Matches.Two);

      return tileIndex;
    },
    // Move of the player (AI)
    robotMove(): void {
      if (this.turn === this.aiPlayer) {
        let tileIndex: number = null;

        switch (this.aiDifficulty) {
          case Difficulty.Easy:
            tileIndex = this.robotRandom();

            break;
          case Difficulty.Medium:
            tileIndex = this.robotAttack(tileIndex);

            if (tileIndex === null) {
              tileIndex = this.robotRandom();
            }

            break;
          case Difficulty.Hard:
            tileIndex = this.robotCombinaton(this.aiPlayer + Matches.Two);

            if (tileIndex === null) {
              tileIndex = this.robotDefend();
            }

            if (tileIndex === null) {
              tileIndex = this.robotCombinaton(this.aiPlayer + Matches.One);
            }

            if (tileIndex === null) {
              tileIndex = this.robotRandom();
            }

            break;
        }

        if (tileIndex !== null) {
          this.makeDecision(tileIndex);
        }
      }
    },
    // Move of the player (human)
    humanMove(elm: object): void {
      if (!elm.classList.contains("active")) {
        let tileIndex: number = elm.dataset.tile;

        this.makeDecision(tileIndex);

        if (this.aiPlayer !== null) {
          this.robotMove();
        }
      }
    },
    // Make decision on rendering the board
    makeDecision(tileIndex: number): void {
      if (tileIndex !== null) {
        this.buttons[tileIndex].active = true;
        this.buttons[tileIndex].player = this.turn;
      }

      if (this.findCombination() === true) {
        this.decideGameWin();

        return;
      }

      if (this.findAnotherTile() === false) {
        this.showDialog = true;

        return;
      }

      this.turn = this.turn === 0 ? 1 : 0;
    },
    // Changes the buttons color into match
    changeButtonsToMatch(): void {
      for (let i: number = 0; i < this.match.length; i++) {
        let j: number = this.match[i];

        this.buttons[j].match = true;
      }
    },
    // Find another available tile
    findAnotherTile(): boolean {
      let hasTile = false;

      for (let i: number = 0; i < this.buttons.length; i++) {
        if (this.buttons[i].active === false) {
          hasTile = true;

          break;
        }
      }

      return hasTile;
    },
    // Game win event
    decideGameWin(): void {
      this.changeButtonsToMatch();

      this.showDialog = true;

      this.winningPlayer = Player[this.turn];
    },
    // Creates the tic tac toe map
    createMap(): void {
      for (let i: number = 0; i < 9; i++) {
        this.buttons.push({
          player: null,
          active: false,
          match: false
        });
      }
    },
    // Event when the tile is clicked
    tileClick(e: object): void {
      if (this.play === true) {
        if (this.turn === this.aiPlayer) {
          return false;
        }

        let elm: object = e.target;

        this.humanMove(elm);
      }
    },
    // Proceed to next step of the dialog
    proceedToNext(): void {
      this.dialogStep += 1;
    },
    // Choose the opponent to play with
    chooseOpponent(isRobot: boolean): void {
      if (isRobot === true) {
        this.proceedToNext();
      } else {
        this.dialogStep = 4;
        this.showDialog = false;
      }
    },
    // Choose your player (VS AI)
    choosePlayer(chosenPlayer: number): void {
      this.aiPlayer = chosenPlayer === 0 ? 1 : 0;

      this.proceedToNext();
    },
    // Choose the AI difficulty
    chooseDifficulty(difficulty: number): void {
      this.aiDifficulty = difficulty;

      this.dialogStep = 4;
      this.showDialog = false;

      if (this.aiPlayer === Player.X) {
        this.robotMove();
      }
    },
    // Start the game with same settings
    playAgain(): void {
      this.buttons = [];
      this.turn = 0;
      this.play = true;
      this.match = null;
      this.winningPlayer = null;
      this.showDialog = false;
      this.dialogStep = 4;

      this.createMap();

      if (this.aiPlayer === Player.X) {
        this.robotMove();
      }
    },
    // Initialize the game
    initialize(): void {
      this.aiPlayer = null;
      this.aiDifficulty = null;

      this.playAgain();

      this.showDialog = true;
      this.dialogStep = 0;
    }
  },
  created(): void {
    this.initialize();
  }
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

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