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

Hammad Ahmed

This tutorial assumes that you have a little prior knowledge of JavaScript and the Vue framework. You also need to have Node and Git installed on your system.

Introduction

This tutorial focuses on building a game with the Vue framework. You will also learn about vue-cli, vue-loader and the workflow of making user interfaces for the web using Vue. At the end of the tutorial, you will have a playable Tic-Tac-Toe game like this below:

What is Tic-Tac-Toe?

In this tutorial, we are going to build a simple tic-tac-toe game with the Vue framework. Tic-Tac-Toe is a two-players pencil and paper game. One player is Cross 'X' and the other one is Nought 'O'. The game is based on a 3x3 grid with each box in the grid having the space to be marked with either X or O. One move consists of one mark. The game is turn-based meaning that players own a move one after the other. Once a mark has been placed, it cannot be altered. So, who wins the game? The one who is able to place 3 consecutive Xs or Os in a line. The line can be vertical, horizontal or diagonal. And to get acquainted with it, search Google for tic-tac-toe and play it the times until you don't want to any longer.

Game Board

Hey Hammad, I already knew all this stuff and I've played it a hundred times before? I know, I know. Forgive me for this intro and let's get started to dive in.

Why Vue

Vue Website

This version of Tic-Tac-Toe is going to be browser-based and implemented using Vue. The reason we are building it with Vue is that there can't be anything simpler than that. And by anything, I mean any other JavaScript framework. You know you can use something else. But in this particular tutorial, you have to stick by me.

Setup

For the setup, we are going to use vue-loader with vue-cli.

vue-cli

vue-cli is a simple CLI (Command Line Interface) tool for scaffolding Vue projects. It provides project boilerplate, allows you to write ES2015, convert processors into plain CSS and handles all the rest. Install vue-cli on your machine using the following command:

npm install -g vue-cli

Now that you vue-cli, run the following commands in the terminal for vue-cli to scaffold the project structure for you:

vue init webpack-simple vue-tic-tac-toe

Here, vue-tic-tac-toe is the name of the project, in which vue-cli will init in. webpack-simple is a template that includes both Webpack and vue-loader.

After that, cd in the vue-tic-tac-toe and install the dependencies using npm:

cd vue-tic-tac-toe
npm install

vue-loader

vue-loader is a loader for Webpack that allows you to write the template and CSS for a component all in one file. The file needs to have .vue extension. This is how an example .vue file looks like:

<template>
    <div class="message">
        {{ speaker }} says: {{ message }} to the <world></world>
    </div>
</template>

<script>
    import World from './World.vue'

    export default {
        components: { World },
        data () {
            return {
                speaker: 'Hammad',
                message: 'I will rule the world'
            }
        }
    }
</script>

<style>
    .message {
        padding: 10px;
        background-color: steelblue;
        color: #fff;
    }
</style>

Create a components folder in the src directory present in the root. It will contain all of our components. Now that everything is ready, run this command to view this app in the browser:

npm run dev

This command will load http://localhost:8080/ in the browser. Every change you make in your code will be reflected in the browser even without refreshing the page. Open App.vue in your code editor and delete all the unnecessary stuff present in the template and script tag. Now your App.vue file looks like this:

<template>
  <div id="app">

  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {

    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

h1, h2 {
  font-weight: normal;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 10px;
}

a {
  color: #42b983;
}
</style>

Game Elements

Thinking of this game, all the things that come to my mind are:

  • 2 Players (X and O)
  • The Board Grid

Grid Board

  • 3 Rows
  • 3 Columns
  • 9 Grid Cells
  • Scoreboard (The number of wins for each player)

Scoreboard

  • Number of match being played

Match Number

  • Player turn

Game Status

  • Restarting the game

Game Restart

We will extract some of them elements into their own components and others as properties of those components.

Vue Components

The benefit of using components is that we can reuse them. Like, we can use the Cell component 9 times, without having the need to duplicate it. This keeps our code DRY (Don't Repeat Yourself). Our component structure will look like this:

-- App
---- Board
------ Cell x9

Create these components: Board.vue and Cell.vue in the components folder with the following boilerplate code.

<template>

</template>

<script>
    export default {
        data () {}
    }
</script>

<style>

</style>

Styling

To make sure that this game doesn't hurt your eyes, you can grab all the styling and fonts from here and paste them in your code.

In the index.html file, I only added two fonts and changed the title. The Dosis font is for the whole body and the Gochi Hand font is for the X and O placed in the grid. Both are taken from Google Fonts. This is how our index.html file looks like:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Tic-Tac-Toe Game</title>
    <link href="https://fonts.googleapis.com/css?family=Dosis|Gochi+Hand" rel="stylesheet">
  </head>
  <body>
    <div id="app"></div>
    <script src="/dist/build.js"></script>
  </body>
</html>

Change the <style> tag of your App component to this:

<style>
body {
  background-color: #fff;
  color: #fff;
  font-family: 'Dosis', Helvetica, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  margin: 0px;
}

#app {
  margin: 0 auto;
  max-width: 270px;
  color: #34495e;
}

h1 {
  text-transform: uppercase;
  font-weight: bold;
  font-size: 3em;
}

.restart {
  background-color: #e74c3c;
  color: #fff;
  border: 0px;
  border-bottom-left-radius: 10px;
  border-bottom-right-radius: 10px;
  font-family: 'Dosis', Helvetica, sans-serif;
  font-size: 1.4em;
  font-weight: bold;
  margin: 0px;
  padding: 15px;
  width: 100%;
}

.restart:hover {
  background-color: #c0392b;
  cursor: pointer;
}

.scoreBoard {
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  align-items: center;
  width: 100%;
  height: 15px;
  background-color: #16a085;
  box-shadow: 10px solid #fff;
  padding: 20px;
  overflow-x: none;
}

.scoreBoard h2 {
  margin: 0px;
}

.scoreBoard span {
  float: right;
  font-size: 1.5em;
  font-weight: bold;
  margin-left: 20px;
}
</style>

Add this to the <style> tag of the Grid component:

.grid {
  background-color: #34495e;
  color: #fff;
  width: 100%;
  border-collapse: collapse;
}

.gameStatus {
  margin: 0px;
  padding: 15px;
  border-top-left-radius: 20px;
  border-top-right-radius: 20px;
  background-color: #f1c40f;
  color: #fff;    
  font-size: 1.4em;
  font-weight: bold;
}

.statusTurn {
    background-color: #f1c40f;
}

.statusWin {
    background-color: #2ecc71;
}

.statusDraw {
    background-color: #9b59b6;
}

And this to that of the Cell component:

.cell {
  width: 33.333%;
  height: 90px;
  border: 6px solid #2c3e50;
  font-size: 3.5em;
  font-family: 'Gochi Hand', sans-serif;
}

.cell:hover {
    background-color: #7f8c8d;
}

.cell::after {
  content: '';
  display: block;
}

.cell:first-of-type {
  border-left-color: transparent;
  border-top-color: transparent;
}

.cell:nth-of-type(2) {
  border-top-color: transparent;
}

.cell:nth-of-type(3) {
  border-right-color: transparent;
  border-top-color: transparent;
}

tr:nth-of-type(3) .cell {
  border-bottom-color: transparent;
}

Component Templates

The template section of a component contains all the markup that makes up the component. Our App component will contain the Gird component and the Gird component will contain 9 Cell components. The App component is very simple and only contains a heading and a grid for the game. We will add more functionality later.

<div id="app">
  <div id="details">
    <h1>Tic Tac Toe</h1>
  </div>
  <grid></grid>
</div>

The Grid component contains a table that has three rows and three cells in each row. The cell number is passed down as a prop to uniquely identify each cell. The template of the Grid component:

<table class="grid">
  <tr>
    <cell name="1"></cell>
    <cell name="2"></cell>
    <cell name="3"></cell>
  </tr>
  <tr>
    <cell name="4"></cell>
    <cell name="5"></cell>
    <cell name="6"></cell>
  </tr>
  <tr>
    <cell name="7"></cell>
    <cell name="8"></cell>
    <cell name="9"></cell>
  </tr>
</table>

The Cell component contains only a <td> tag to hold the mark X or O.

<td class="cell">{{ mark }}</td>

The Game Flow

To start adding functionality to our game, we need to determine the flow of events which will take place with each user interaction. The flow of the game is as follows:

  • The App is loaded.
  • All cells are empty.
  • O is the first player.
  • The player can place an O in any of the cells.
  • The player-turn is changed X.
  • Each time a player places a mark, the turn is handed to the non-active player.
  • After each strike, we need to check if the game meets any winning condition.
  • We also need to check if the game is a draw.
  • After or anytime in between a game, a button called Restart can be clicked to restart the game.
  • The status of the game that is if the game is in progress or won or is a draw.
  • In terms of progress, the status displays the turn of the respective player.
  • The game also displays the number of matches and the number of wins for each player.

For all of this to be able to happen, our components need some data properties and methods.

Data Properties

We will divide the data among the components according to their relation or ease of access to that component.

The App component will hold the number of matches and the number of wins for each player.

data () {
    return {
      matches: 0,
      wins: {
        O: 0,
        X: 0
      }
    }
}

The Grid component holds the data for the active player (X or O), the game status, status message, status color (for displaying on the top bar), the number of moves played by both players to (check for a draw), the mark placement for each cell and all (8) the winning conditions. The winning conditions array contains 8 arrays, and each array contains possible winning same cell (all X or all O) mark arrangement of cell number. These conditions can be compared with the cells object to check for a win.

data () {
  return {
      // can be O or X
      activePlayer: 'O',
      // maintains the status of the game: turn or win or draw
      gameStatus: 'turn',

      gameStatusMessage: `O's turn`,
      // status color is used as background color in the status bar
      // it can hold the name of either of the following CSS classes
      // statusTurn (default) is yellow for a turn
        // statusWin is green for a win
        // statusDraw is purple for a draw
      gameStatusColor: 'statusTurn',
      // no. of moves played by both players in a single game (max = 9)
      moves: 0,
      // stores the placement of X and O in cells by their cell number
        cells: {
            1: '', 2: '', 3: '',
            4: '', 5: '', 6: '',
            7: '', 8: '', 9: ''
        },
        // contains all (8) possible winning conditions
        winConditions: [
            [1, 2, 3], [4, 5, 6], [7, 8, 9], // rows
            [1, 4, 7], [2, 5, 8],    [3, 6, 9], // columns
            [1, 5, 9], [3, 5, 7]             // diagonals
        ],
  }
}

The Cell component holds the mark that the player placed in it. By default that value is set to an empty string. The frozen property is used to ensure that the player is not able to change the mark, once it is placed.

props: ['name'],
data () {
    return {
     // enables the player to place a mark
     frozen: false,

        // holds either X or O to be displayed in the td
        mark: ''
    }    
}

The Cell component also has a name prop for uniquely identifying each cell with a number.

Event Bus

Our components need to talk to each other to inform them about a change in their data property or an action performed by the user like placing a mark in a cell. To do so, we will use an event bus. To make an event bus, we will assign a new Vue instance to a property called Event on the window object. You can change this to anything you want depending on the context of your code, but Event, here, will do just fine.

window.Event = new Vue()

With it, you can do things like Event.$emit() and Event.$on for firing and listening to events respectively. The event name as the first argument. You can also include any other data after the name argument. The other data is called Payload. Consider this example:

Event.$emit('completed', this.task)

This fires an event called completed and passes this.task as payload. You can listen to this event fire with the Event.$on() method like this:

Event.$on('completed' (task) => {
    // do something
})

To continue listening to an event fire, we can place the Event.$on method in the created method of that Vue component.

created () {
    Event.$on('completed' (task) => {
        // do something
    })
}

After adding the Event bus to your main.js file, it will look like this:

import Vue from 'vue'
import App from './App.vue'

window.Event = new Vue()

new Vue({
  el: '#app',
  render: h => h(App)
})

Now, we are ready to fire and listen for events in our game.

What's Next

We will continue, adding functionality to our game, in a follow-up lesson of this two-part series.

Conclusion

In this tutorial, we wireframed almost all the parts of our game. We have added styling, templates and data properties and distributed them among components. At the end, we also created an event bus to handle all of the data flow and user action notifications between our components. If you did not undertand something, experienced a bug or have some other question, feel free to ask in the comments below.

Hammad Ahmed

6 posts

Laravel and Vue developer. Available for freelance work.