DEV Community

Cover image for Tic Tac Toe Game with Vue JS
RF Fahad Islam
RF Fahad Islam

Posted on

Tic Tac Toe Game with Vue JS

Introduction

This is an overview of the tic tac toe game made with Vue3. Using Vue3 composition API and reactivity features makes the rendering process to DOM much easier. However, If you want the tutorial for making this game with Vue3 or Vanilla Js from scratch, let me know in the comment section. I'll explain the logic-building process for the game.

๐Ÿ˜€My Hashnode Blogsite : Visit

Overview

image.png
Link to the game: Tic-Tac-Toe

Source Code

GitHub Repo: Vue-Game-Project
The main Logic is in the src>component>Board.vue file

โ˜Cloud Codespace : Space

Full Tutorial is Coming Soon .....

Board.vue(Main) File Code

<template>
<div class="container">
    <!-- <span  style="cursor: pointer; font-size: 35px; position: absolute; left: 20px;" class="float-start" @click="isMuted = !isMuted">
    <i class="fa fa-volume-up" v-if="!isMuted"></i>
    <i class="fa fa-volume-off" v-else></i>
    </span> -->
  <header>
      <h1 class="my-2">
        <img src="../assets/logo.png" lazy alt="Logo" width="40" /> Tic Tac Toe
    <div class="form-check form-switch d-inline-block" id="toggle" style="font-size: 20px;" v-show="!gameStart">
      <input
        class="form-check-input"
        type="checkbox"
        role="switch"
        v-model="isRow"
      />
    </div>

      </h1>
  </header>

  <div :class="[isRow?'row':'text-center']">

  <!-- !Controls and Message Section -->
  <section :class="[isRow?'col-md-6 col-sm-12':'']">
      <!-- Computer Switch -->
    <div class="form-check form-switch switch" v-show="!gameStart">
      <input
        class="form-check-input"
        type="checkbox"
        role="switch"
        id="flexSwitchCheckDefault"
        v-model="isComputer"
        :disabled="gameStart"
      />
      <label class="form-check-label mx-2" for="flexSwitchCheckDefault"
        >Computer</label>
    </div>


    <transition-group
    name="custom-classes-transition"
    enter-active-class="animated bounceIn"
    leave-active-class="animated bounceOut"
    tag="ul"
  >
    <h1 class="text-black" v-if="gameStart">Round : {{round}}</h1>


      <h4 v-if="gameStart&&isComputer" style="
        border: 2px solid skyblue;
      " class="alert font-weight-bold text-black d-inline-block">
        <span>
          <i class="fa fa-desktop text-primary"></i> Computer in
          <i class="fa fa-dashboard text-black mx-1"></i>
             <strong class="text-decoration-underline">   {{ level }} </strong> mode
        </span>
      </h4>
    </transition-group>

    <div
      v-if="!gameStart && isComputer"
      class="d-flex justify-content-center my-2"
    >
      <select
        name="level"
        v-model="level"
        id="level"
        class="form-select"
        :disabled="usedBoxes > 0"
      >
        <option value="hard" default>Choose Difficulty Level (Default Hard)</option>
        <option value="noob">Noob</option>
        <option value="easy">Easy</option>
        <option value="medium">Medium</option>
        <option value="hard">Hard</option>
      </select>
    </div>

    <h4
      class="p-2 alert-warning d-inline-block rounded-2"
      v-if="!isComputer"
    >
      Player Move : "{{ player }}"
    </h4>
 <!-- </transition-group> -->

    <h1 v-if="winner == 'draw'" :style="!isComputer?{fontSize: '50px'}:{}" class="alert alert-warning">Its A Draw!</h1>
    <h1
      class="text-center mx-2 alert alert-success"
      :class="[isComputer && winner === 'O' ? 'alert-danger' : 'alert success']"
      v-if="winner != 'draw' && winner"
    >
      <span v-if="!isComputer">
        <strong v-if="winner != 'draw'">๐ŸŽ‰ Player {{ winner }} wins! </strong>
      </span>
      <span v-else
        >๐ŸŽ‰
        <strong v-if="winner == 'X'">You</strong>
        <strong v-else>Computer</strong>
        Wins!
      </span>
    </h1>

    </section>

    <!-- !Game Board Section -->
  <section :class="[isRow?'col-md-6 col-sm-12':'']">

  <!-- !Main Game Board -->
    <div class="board d-block margin-auto">
      <div v-for="(row, x) in squares" :key="x" class="row">

        <div
          v-for="(square, y) in row"
          :key="y"
          @click="move(x, y)"
          class="col square"
          :class="[winBoxes[x][y]==='win'?'winBox':'', squareClasses[x][y]]"
        >
    <transition
    name="custom-classes-transition-{{x}}-{{y}}"
    enter-active-class="animated bounceIn"
    leave-active-class="animated bounceOut">
          {{ square }}
    </transition>
        </div>
      </div>
    </div>
    </section>

  </div>
    <!-- </transition-group> -->


      <section class="d-flex justify-content-around">
        <div
          style="border-radius: 16px"
          class="btn btn-block btn-outline-danger my-2 ml-2"
          @click="restart"
        >
          Restart <i class="fa fa-refresh"></i>
        </div>

        <div
          style="border-radius: 16px"
          class="btn btn-block btn-outline-success my-2"
          @click="next"
        >
          Next Round <i class="fa fa-arrow-right"></i>
        </div>
      </section>
    <!-- !Game Status - Points, Round -->
    <footer class="mt-3 gameStatus" >
      <div class="d-flex p-1 flex-wrap justify-content-around">
        <h5 class="rounded-pill text-primary font-weight-bolder">
          <span v-if="isComputer"><i class="fa fa-user"></i> You</span>
          <span v-else><i class="fa fa-user-circle"></i> Player X</span>:
          <span class="font-weight-bold">{{ playerXPoints }}</span>
        </h5>
        <!-- <h5 class="p-2 rounded-pill d-block font-weight-bold text-black">
          <span><i class="fa fa-gamepad"></i> Round : </span>
          <span class="font-weight-bold" style="font-weight: bold">{{
            round
          }}</span>
        </h5> -->
        <h5 class="rounded-pill text-danger">
          <span v-if="isComputer"><i class="fa fa-desktop"></i> Computer</span>
          <span v-else><i class="fa fa-user-circle"></i> Player Y</span> :
          <span class="font-weight-bold">{{ playerOPoints }}</span>
        </h5>
      </div>
    </footer>

  </div>
</template>

<script>
import { reactive, toRefs, ref, computed} from "vue";
export default {
  name: "Board",
  setup() {
    //* Initialize all sounds for the game
    const audio = new Audio("musics/ting.mp3");

    const squares = ref([
      ["", "", ""],
      ["", "", ""],
      ["", "", ""],
    ]);

    const utils = reactive({
      player: "X",
      gameEnd: computed(() => {
        return Boolean(winner.value);
      }),
      color: computed(() => {
        return utils.gameEnd ? "rgb(233,233,233)" : "none";
      }),
      level: "hard",
      isComputer: true,
      usedBoxes: computed(() => {
        return squares.value.flat().filter((x) => x !== "").length;
      }),
      leftBoxes: computed(() => {
        return squares.value.flat().filter((x) => x === "").length;
      }),
      gameStart: computed(()=> {
        return utils.usedBoxes>0||utils.round>1
      }),
      playerXPoints: 0,
      playerOPoints: 0,
      round: 1,
      isRow: true,
      playFont: computed(()=> {
        if (utils.isRow) return "75px"
        else return "75px"
      }),
      squareSize: computed(()=> {
        if(utils.isRow) return "120px"
        else return "100px"
      }),
      winBoxes : [
        ["","",""],
        ["","",""],
        ["","",""]
      ],
      conditionThatMatched : "",  
      //Classes for squares
      squareClasses: [
        ["top left","top middle","top right"],
        ["left", "middle","right"],
        ["bottom left","bottom middle","bottom right"]
      ],
      soundsPath: "musics",
      //Marking color to mark the win conditions used in css
      markingColor: computed(()=> {
          return "#ff0111" //Red
      }),
      isMuted: false,
    });

    const winConditions = [
      //Vertically- rows
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      //Horizontally- columns
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      //Corner
      [0, 4, 8],
      [2, 4, 6],
    ];

    //Play Sound Method
    const playSounds = async (action) => {
      if(action==="move"){
        audio.play();
      }
    }

     const winner = computed(() => {
      const win = findWinner(squares.value.flat());
      if (!win && utils.usedBoxes === 9) {
        updatePlayerPoints("draw");
        return "draw";
      }
      if(win==="X"||win==="O") updateWinBoxes(utils.winBoxes.flat())
      if(win) playSounds("move")
      updatePlayerPoints(win);
      return win;

    });

    //* For marking the boxes which matches the win conditions
    //* Take utils.winBoxes.flat() as argument
    const updateWinBoxes = (winBoxesFlat)=> {
      utils.conditionThatMatched.forEach((c)=> {
        winBoxesFlat[c] = "win"
      })
      utils.conditionThatMatched = "" //Empty the conditions
      utils.winBoxes = array1dTo2d(winBoxesFlat)
    }

    const move = (x, y) => {
      if (squares.value[x][y] === "" && !utils.gameEnd) {
        squares.value[x][y] = utils.player;
        playerSwap(); //!Swap the player to "O"
        if (utils.isComputer&&!winner.value) executeComputer(); //!Running Computer Move
      }
    };

    const executeComputer = () => {
      if (utils.player === "O" && utils.usedBoxes < 9) {
        const newSquares = computerChoice(squares.value.flat());
        squares.value = newSquares;
        // playSounds("move")
      }
    };

    //* Swap player between 'X' and 'O'
    const playerSwap = () => {
      utils.player = utils.player === "X" ? "O" : "X";
    };

     //* Generating Computer choice
     //! Dealing with flat array of 2D squares array
    const computerChoice = (square) => {
      let choice;
      choice = smartChoice(square, utils.level);
      if (choice === null) {
        do {
          choice = Math.floor(Math.random() * 9); //Random number between 0-8
        } while (square[choice] !== "");
      }
      square[choice] = utils.player;
      playerSwap(); //!Swap the player to 'X' again
      return array1dTo2d(square)
    };

    //* Player is X or O and square is squares.value.flat()
    const findWinChances = (square, player)=> {
      for (let conditions of winConditions) {
          const [a, b, c] = conditions;
          if (square[a] === "" && square[b] === player && square[c] === player) return a;
          else if (square[b] === "" && square[a] === player && square[c] === player) return b;
          else if (square[c] === "" && square[a] === player && square[b] === player) return c;
      }
          return null
    }

    //? Making the computer understand the action based on the positions and user actions
    const smartChoice = (square, level) => {
        let returnPosition;
        //! Level Hard
        if(level==="hard"){
          //* Priotorize computer's win possibilities
          returnPosition = findWinChances(square, "O")
          //*If there is no chances for computer then find the user win possibilites and prevent from winning
          if(returnPosition == null) returnPosition = findWinChances(square, "X")
        }

        //! Level Medium
        else if (level==="medium") {
          //* Prevent the user from winning
          returnPosition = findWinChances(square, "X")
        }
        else if (level==="easy") {
          //* Only focuses on computer win. Don't prevent user from winning
          returnPosition = findWinChances(square, "O")
        }
        else if (level==="noob"){
          //* Randomly Make all Choices
          returnPosition = null
        }
        return returnPosition
    };

    //* For converting squares flat array to 2D again
    const array1dTo2d = (array)=> {
      const newArray = [];
      while (array.length) newArray.push(array.splice(0, 3));
      return newArray;
    }

    //* Update the points
    const updatePlayerPoints = (winner) => {
      if (winner) {
        if (winner === "X") {
          utils.playerXPoints = utils.playerXPoints + 1;
        } else if (winner === "O") {
          utils.playerOPoints = utils.playerOPoints + 1;
        }
      }
    };

    //* find winner along with the winConditions
    //* Returns the winner 'X' or 'O'
    const findWinner = (square) => {
      for (const conditions of winConditions) {
        const [a, b, c] = conditions;
        if (square[a] && square[a] === square[b] && square[a] === square[c]) {
          utils.conditionThatMatched = [a,b,c] //For accessing the boxes
          return square[a]; // Returns player that matches all conditions
        }
      }
      return "";
    };

    const restart = () => {
      if (confirm("Do you really want to restart the game?")) {
        utils.player = "X";
        utils.playerXPoints = 0;
        utils.playerOPoints = 0;
        utils.round = 1;
        squares.value = [
          ["", "", ""],
          ["", "", ""],
          ["", "", ""],
        ];
      utils.winBoxes = [
        ["", "", ""],
        ["", "", ""],
        ["", "", ""],
      ]
      }
    };

    const next = () => {
      if (winner.value) {
        utils.player = "X";
        squares.value = [
          ["", "", ""],
          ["", "", ""],
          ["", "", ""],
        ];
        utils.winBoxes = [
          ["", "", ""],
          ["", "", ""],
          ["", "", ""],
        ]
        utils.round += 1;
      } else {
        alert("Can't go to next round until finis current round.");
      }
    };


    return {
      ...toRefs(utils),
      move,
      squares,
      winner,
      next,
      restart,
    };
  },
};
</script>

<style>
@import url("https://fonts.googleapis.com/css2?family=Kanit:wght@500&family=Varela+Round&display=swap");
.board {
  max-width: fit-content;
  display: block;
  margin: auto;
}
:root {
  animation-duration: 1s;
}
h1 {
  font-family: "Varela Round", sans-serif;
  font-size: 2rem;
}

.alert {
  padding: 8px !important;
  border-radius: 30px!important;
}

.square {
  color: black;
  width: v-bind(squareSize);
  height: v-bind(squareSize);
  border: 2px solid black;
  border-radius: 2px;
  display: inline-block;
  margin: 0px;
  padding: 0px;
  font-size: v-bind(playFont);
  text-align: center;
  cursor: pointer;
  background: v-bind(color);
  font-family: "Varela Round", sans-serif;
}
.square:hover {
  background: rgb(233,233,233);
}

.winBox {
  color: v-bind(markingColor);
  background: rgb(255, 255, 255)!important;
}

.activeBox {
  background: skyblue;
}

.switch {
  display: flex;
  justify-content: center;
  font-size: 29px;
  cursor: pointer !important;
}
input {
  cursor: pointer;
}
h5,
h4,
h3 {
  font-family: "Kanit", sans-serif;
}

.gameStatus {
  font-size: 30px !important;
}
footer {
  border: 2px dashed rgb(68, 68, 68);
  border-radius: 30px;
}

#level{
  width: 330px;
}

/* Squares Classes */
.top{
  border-top: none!important;
}

.bottom{
  border-bottom: none!important;
}

.left{
  border-left: none!important;
}

.right{
  border-right: none!important;;
}

@media screen and (max-width: 766px) {
  #toggle{
    display: none;
  }  
}
@media screen and (max-width: 370px) {
  .square {
    width: 90px!important;
    height: 90px!important;
    font-size: 60px!important;
  }
  #level {
    width: 250px;
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)