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
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>
Top comments (0)