Building a Tic-Tac-Toe Game with Vue 2: Part 2

Hammad Ahmed

Part 1

Tic Tac Toe Game

In the previous tutorial, we determined the elements of the game, applied styling and added the data properties. In this tutorial, we will take up the code, where we left off.

Introduction

In this part, we will finish our game, applying all the functionality to make it fully playable. We will make heavy use of the event bus we implemented in the previous part. Together, we will implement everything remaining from listening for strikes, checking a win or draw, to displaying the game status and the scoreboard. At the end, we will also give our players the ability to restart the game and display the number of matches played. So, let's get started.

Listening For Strikes

To let the player place a mark in any of the cells, we need to let him click on it and listen for that action. In the Cell component add the v-on:click attribute to the <td> tag. We will use the @click shorthand syntax. To the click, we respond using the strike method.

<td class="cell" @click="strike">{{ mark }}</td>

In the strike method, we need to get the active player from its parent component that is the Grid component and use it to change the mark property. The frozen property is set to true because we don't want any player to replace an already marked cell. We also need to tell the Grid component about this change. We will fire the strike event for the Grid component to listen with the cell name prop as payload.

methods: {
    strike () {
        if (! this.frozen) {
            // gets either X or O from the Grid component
            this.mark = this.$parent.activePlayer

            this.frozen = true

            // fires an event to notify the Grid component that a mark is placed
            Event.$emit('strike', this.name)
        }
    }
}

Now, the Grid component needs to listen for this event fire and respond accordingly. According to the game flow, the Grid component now needs to fill the respective cell number with the mark in the cells object, increment the number of moves, change the game status and change the active player to the non-active player. The check for game status also checks if the game has been won or is a draw. Add the following code to the created method of the Grid component so that we are always listening to this event fire.

// listens for a strike made by the user on cell
// it is called by the Cell component
Event.$on('strike', (cellNumber) => {
        // sets either X or O in the clicked cell of the cells array
        this.cells[cellNumber] = this.activePlayer

        // increments the number of moves
        this.moves++

        // stores the game status by calling the changeGameStatus method
        this.gameStatus = this.changeGameStatus()

        this.changePlayer()
})

The first two lines of code only set the data properties. The next two lines call methods. We need to create those two methods. The changePlayer() method is very simple. All it does is that it changes the player to the non-active player. If the active player is O, it changes the active player to X and if the active player is X, it changes it to O. To clean up our code, we can use a computed property to get nonActivePlayer. This computed property will handle this task and leave little for the changePlayer() method to do. The computed property will look like this:

computed: {
    // helper property to get the non-active player
    nonActivePlayer () {
        if (this.activePlayer === 'O') {
            return 'X'
        }

        return 'O'
    }
}

And the changePlayer() method looks like this.

// changes the active player to the non-active player with the help of the nonActivePlayer computed property
changePlayer () {
    this.activePlayer = this.nonActivePlayer
},

Displaying the Status

While listening for the strike event, we also changed the value of the gameStatus property using the changeGameStatus() method. We need to create it now. In this method, we need to check if the game is a win or a draw or is in progress. We will check for a win using the checkForWin() method, which we will create later. If the win condition is met, we return the gameisWon() method, which we will create later too. And to check for a draw, we make sure that the game is not yet won by any of the players and all the cells are filled. If none of them meets the conditions, we simply return 'turn', that is the default value of the gameStatus

// returns the game status to the gameStatus property
changeGameStatus () {
    if (this.checkForWin()) {
         return this.gameIsWon()
    // checks if the game is still not won and all cells are filled
    } else if (this.moves === 9) {
        // sets the status to draw
        return 'draw'
    }
    // sets the status to turn
    return 'turn'
}

By returning strings, we are setting the value of the gameStatus property.

// stores the game status by calling the changeGameStatus method
this.gameStatus = this.changeGameStatus()

Depending upon the value of gameStatus, we need to change other properties too, like gameStatusMessage and gameStatusColor. We can make use of the watch feature available in Vue. The watch object listens for any change in any of the data property and helps you to perform certain actions.

Using this, we can change the value of gameStatusMessage and gameStatusColor if there is a change in the value of gameStatus.

watch: {
    // watches for change in the value of gameStatus and changes the status 
    // message and color accordingly
    gameStatus () {
        if (this.gameStatus === 'win') {
            this.gameStatusColor = 'statusWin'

            this.gameStatusMessage = `${this.activePlayer} Wins !`

            return
        } else if (this.gameStatus === 'draw') {
            this.gameStatusColor = 'statusDraw'

            this.gameStatusMessage = 'Draw !'

            return
        }

        this.gameStatusMessage = `${this.activePlayer}'s turn`
    }
}

The code above is pretty straightforward. It sets the status message and color using the value of gameStatus. In this code, I am using Template Strings, that is a newly added feature to ES6. The problem here is that many browsers do not understand the new JavaScript syntax. But that is no issue since we are using the vue-loader setup. It uses Babel to compile the code for older browsers to understand.

We can display the status using these data properties to the player. Add the following <div> to the template tag of the Grid component just before the <table> tag.

<div class="gameStatus" :class="gameStatusColor">
    {{ gameStatusMessage }}
</div>

:class is the shorthand syntax for v-bind:class. It helps to dynamically toggle classes depending upon the value of an attribute.

Checking for a Win

While writing the code to change the game status, we called the checkForWin() method and also the gameIsWon(). We need to implement them now. If you can remember, at the beginning of this tutorial, we added an array called winConditions as a data property. We are going to use that now. To check for a win, we will loop through each array in the winConditions array as the cell number and check if the three of them have the same mark that is X or O.

// checks for possible win conditions from the data
checkForWin () {
    for (let i = 0; i < this.winConditions.length; i++) {
        // gets a single condition wc from the whole array
        let wc = this.winConditions[i]
        let cells = this.cells

        // compares 3 cell values based on the cells in the condition
        if (this.areEqual(cells[wc[0]], cells[wc[1]], cells[wc[2]])) {
            return true
        }
    }

    return false
}

This method uses an extra helper function areEqual to keep the code clean. The areEqual method looks like this:

// helper function for comparing cell values
areEqual () {
   var len = arguments.length;

   // loops through each value and compares them with an empty sting and 
   // for inequality
   for (var i = 1; i < len; i++){
      if (arguments[i] === '' || arguments[i] !== arguments[i-1])
         return false;
   }
   return true;
}

This method takes all arguments, loops through them, checks it against an empty string and returns false if the argument is not equal to the previous argument. If this loop passes, meaning that is does not return false, it finally returns true.

The gameIsWon() method still remains to be implemented. We will do four things in this method. Firstly, we will fire an event called win with the activePlayer as payload and then return a string containing win to be placed in the gameStatus property. With this event fire, we can manage the number of wins for each player in the game and display it on the scoreboard. Secondly, we set the status message. Thirdly, we fire an event to freeze the cells. And at last, return a string 'win'.

gameIsWon () {
    // fires win event for the App component to change the score
    Event.$emit('win', this.activePlayer)

        // sets the game status message
        this.gameStatusMessage = `${this.activePlayer} Wins !`

        // fires an event for the Cell to freeze
        Event.$emit('freeze')

    // sets the status to win
    return 'win'
}

We need to listen for this listen and set the frozen property of the Cell component to true. Add this code to the created method of the Cell component.

Event.$on('freeze', () => this.frozen = true)

Scoreboard

Game Scoreboard

The App component needs to listen for the win event that we fired in the gameIsWon method. All we need to do when listening for this event is to increment the number of wins for the respective player. The player will be available to us as it was passed as payload in the event fire. Add the following to the created method of the App component.

created () {
  Event.$on('win', winner => this.wins[winner]++)
}

As we have the number of wins, we can display it to the player. Add this markup to the template section of the App component just above the <div> with an id of app.

<div class="scoreBoard">
  <span>O has {{ wins.O }} wins</span>
  <h2>Score Board</h2>
  <span>X has {{ wins.X }} wins</span>
</div>

You need to enclose the whole markup in the template in a parent <div>, so that Vue does not squawk about this. We can only have a single topmost-level element in the template.

Restarting the Game

Restart button

Almost everything is done now, but our players can have only one match per browser load. That is not fair. Let's add a restart button so that our players can play as many matches as they want. Add the following button in the template tag of the App component just after the <grid> tag.

<button class="restart" @click="restart">Restart</button>

This button listens for clicks and calls the restart method on each click. We need to add the restart method for this button to work. The things that we need to do in this method are to clear the mark property of all the cells and reset all the data of the Grid component. This way, we will have a fresh new game to play.

restart () {
  Event.$emit('clearCell')

  Event.$emit('gridReset')

  this.matches++
}

Here, we are calling two methods and incrementing the number of matches. The first event fire is for the Cell component to clear all the cells. And the second event fire is for the Grid component to reset its data. We need to listen for both the event fires.

The first one is very simple. In the Cell component, we will set the mark property to an empty string so that it can be replaced with a mark by any player. We also toggle the frozen property to false so that our player can place a mark. Add this code to the created method of the Cell component.

Event.$on('clearCell', () => {
    this.mark = ''

    this.frozen = false
})

For the second event fire, the Grid component will reset all its data property. To do so, we will make use of the Object.assign() function. It accepts the target object as the first argument and the source object as the second argument. Add this code to the created method of the Grid component.

// listens for a restart button press
// the data of the component is reinitialized
// it is called by the App component
Event.$on('gridReset', () => {
    Object.assign(this.$data, this.$options.data())
})

The $data returns the whole data object and $options.data() return the initial state of the data as set in the object. So, in this code, the initial data is set to the data object, which in other words, resets the data of the component.

Number of Matches

Title and matches count

At last, we can display the number of the match being played by the player. Add this <h2> tag below the <h1> in the App component.

<h2>Match #{{ matches + 1 }}</h2>

We add 1 in it because we want to display the number of the match being played at the time and not the number of matches played before the game that is being played.

Conclusion

The final version of the game

Along these tutorials, we learned many important concepts required for making frontend apps. We learned about bootstrapping a Vue app with vue-cli and vue-loader. We also learned about Eventing and made an event bus. We also made use of computed properties, the watch method.

That's it for this 2 part series. I hope you enjoyed making and playing this game with me and yourself. If you have any question, you may ask in the comments below.

Hammad Ahmed

Laravel and Vue developer. Available for freelance work.