In our previous tutorial, we have made the tic tac toe game in javascript using modern class-based syntax added in ES6. In this post, we are going to clone the same game in VueJS instead. If you are learning Vue js or just want to know how can we convert existing JS applications to vuejs you can take some hint from this post.
We have tried to reuse our backend codes as much as possible. The backend classes Player and Game which we’ve created in our past tutorial is fully usable here without any modification. We should add export default at the end of each class to make them importable in our Vue file.
To begin with, making tic tac toe game in Vue js let’s start with creating an empty vuejs project.
vue init webpack tictactoe
It’ll scaffold a vuejs project on the respective directory. You should open your project files in visual studio code with “code .” in cmd.
For starting webpack compilation in dev mode write the command: npm run dev in cmd.
After opening your project in vscode, rename the file named HelloWorld.vue to GameUI.vue and change it’s import details in App.vue too.
After that create new files in src directory with name Player.js and Game.js.
The final directory structure would be like below.

- Contents for Player.js
We are utilizing the same class as in our previous tutorial. To know more about it’s implementation visit our previous tutorial.
class Player { constructor(sign) { this.sign = sign; this.clicks = []; this.score = 0; this.combinations = []; this.positions = [] } setPosition() { this.positions = this.clicks.join(",").split(","); } sortClicks() { this.clicks.sort(); } } export default Player;
- Contents for Game.js
class Game { constructor() { this.players = [] this.winningCondition = [ ["00", "01", "02"], ["00", "11", "22"], ["00", "10", "20"], ["02", "11", "20"], ["10", "11", "12"], ["02", "12", "22"], ["01", "11", "21"], ["20", "21", "22"] ]; this.currentTurn = "X"; this.drawScore = 0; this.isGameEnded = false; this._winningConditions = this.winningCondition.map(x => x.join(",")); } changeTurn() { this.currentTurn = this.currentTurn == this.players[0].sign ? this.players[1].sign : this.players[0].sign; } reset() { this.currentTurn = this.players[0].sign; this.players.forEach(x => { x.clicks = []; }) } checkWinner(btn) { if (btn.innerHTML == this.players[0].sign) { this.players[0].clicks.push(btn.id); } else { this.players[1].clicks.push(btn.id); } this.players.forEach(x => { x.sortClicks() x.setPosition(); }) if (this.players[0].positions.length > 2 || this.players[1].positions.length > 2) { let player1_combinations = this.getCombination(this.players[0].positions, 3, 0); console.log(player1_combinations) let player2_combinations = this.getCombination(this.players[1].positions, 3, 0); //find p1,p2 common with winning conditions let player1_common = this._winningConditions.filter(value => player1_combinations.includes(value) ); let player2_common = this._winningConditions.filter(value => player2_combinations.includes(value) ); return this.isGameOver(player1_common, player2_common); } else { return false; } } isGameOver(player1_common, player2_common) { if (player1_common.length < 1 && player2_common.length < 1) return false; let gameOver = false; if (player1_common > player2_common) { gameOver = true this.players[0].score += 1; alert("Player One Won"); } else if (player2_common > player1_common) { gameOver = true this.players[1].score += 1; alert("Player Two Won"); } else if (this.players[0].clicks.length > 4 || this.players[1].clicks.length > 4) { gameOver = true this.drawScore += 1; alert("Draw"); } else { gameOver = false } return gameOver; } getCombination(input, len, start) { const result = new Array(3); let combinations = new Array(); combine(input, len, start); function combine(input, len, start) { if (len === 0) { combinations.push(result.join(",")); return; } for (var i = start; i <= input.length - len; i++) { result[result.length - len] = input[i]; combine(input, len - 1, i + 1); } } return combinations; } } export default Game;
- Contents for GameUI.vue
GameUI is the UI component for tictactoe game. As it’s a Vue project it has 3 sections: template, script, and style.
script: All js code
style: (scoped: limited to this file only), UI styles
template: HTML/design structure
This component is slightly different than our previous example as we don’t have to write any codes to display the content on the screen. We are utilizing the concept of data binding which makes the instant update in the UI when some variable values are changed.
<template> <div> <div class="text-center"> <h1>Tic Tac Toe</h1> <h3 id="turnText">Turn Of :{{this.game.currentTurn}}</h3> <div class="score" v-if="game.players"> <span> <u>Scores</u> </span> <br /> <span id="p1_score">Player One :{{this.game.players[0].score}}</span> <span id="p2_score">Player Two :{{this.game.players[1].score}}</span> <span id="draw_score">Draw :{{this.game.draw_score}}</span> </div> <button class="replay" id="restartBtn" @click="resetGame()">Restart</button> </div> <div class="gamebox" style="margin-top:20px"> <button id="00" @click="onClick($event)">{{btnText["00"]}}</button> <button id="01" @click="onClick($event)">{{btnText["01"]}}</button> <button id="02" @click="onClick($event)">{{btnText["02"]}}</button> <button id="10" @click="onClick($event)">{{btnText["10"]}}</button> <button id="11" @click="onClick($event)">{{btnText["11"]}}</button> <button id="12" @click="onClick($event)">{{btnText["12"]}}</button> <button id="20" @click="onClick($event)">{{btnText["20"]}}</button> <button id="21" @click="onClick($event)">{{btnText["21"]}}</button> <button id="22" @click="onClick($event)">{{btnText["22"]}}</button> </div> </div> </template>
This section is a slightly modified version of our original HTML. @click is added to the buttons and $event is passed as an argument to identify the sender. btnText will store the respective signs clicked on the button. The score and player turn are displayed directly from the game object which is our main object acting as a game manager.
<script> import Player from "../Player"; import Game from "../Game"; const game = new Game(); game.players.push(new Player("X")); game.players.push(new Player("O")); export default { name: "GameUI", data() { return { game: game, buttons: [], btnText: [] }; }, methods: { onClick($event) { let btn = $event.target; //check if filled already if (this.btnText[btn.id.toString()].length > 0) { alert("Already filled"); return; } //fill X/O this.btnText[btn.id] = this.game.currentTurn; this.game.changeTurn(); //check if game won setTimeout(() => { if (this.game.checkWinner(btn) == true) { this.resetGame(); } }, 100); }, resetGame() { //UI and backend reset this.game.reset(); [...this.buttons].forEach(btn => { this.btnText[btn.id] = ""; }); } }, mounted() { //load all buttons and reset initially this.$nextTick(() => { this.buttons = this.$el.querySelectorAll(".gamebox button"); this.resetGame(); }); } }; </script>
This section contains the major game UI logic. Initially, we import our backend modules Player and Game and initialize them. GameUI object has 3 objects : game , buttons and btnText. game is the game manager object imported above. buttons are the list of buttons clickable by player and btnText holds the text of each button.
In a mounted event, we load all the buttons and store them to buttons variable.resetGame() method will reset the UI at the beginning/end of gameplay or even in case of clicking the restart button.
onClick() function will be executed each time the player clicks the button. After hitting the button we initially check whether that button is already clicked. if clicked: we’ll show an error message. We can also disable the buttons after clicking, but we are not doing that here. After that, we fill the btnText with player’s sign and then we change the player’s turn. We’ll then check if the game is complete / not and whether someone has won the game or not with checkWinner function in-game module.
<style scoped> .gamebox button { width: 100px; height: 100px; border: 1px solid #000; display: inline-block; font-size: 30px; color: #fff; } .gamebox { margin: 0 auto; max-width: 300px; display: grid; justify-items: center; align-content: space-around; grid-template-columns: repeat(3, 1fr); } .replay { border: 1px solid #ddd; height: 60px; margin-top: 20px; padding: 10px; } </style>
App.vue: it’s the major entry point of our game holding our GameUI component. it would look like this.
<template> <div id="app"> <GameUI /> </div> </template> <script> import GameUI from "./components/GameUI"; export default { name: "App", components: { GameUI } }; </script>

You can find the source code of this project in our GitHub repository. Please feel free to comment if you’ve faced any problem or didn’t understand any of the code written here. Thank You.