Building a Multiplayer TicTacToe Game with Meteor

Share this article

Wooden game of tic-tac-toe
Wooden game of tic-tac-toe. Build a multiplayer game of tic-tac-toe with Meteor

Meteor is a popular, full stack web framework that makes it very easy to prototype your ideas and get from development to production really fast. Its reactive nature and the use of DDP, make it a great candidate for building simple, multiplayer, browser games.

In this tutorial, I’ll show you how to build a multiplayer TicTacToe with Meteor, using its default front-end templating engine, Blaze. I will assume that you have played around with Meteor a bit, and of course, that you feel comfortable coding with JavaScript.

If you have zero experience with Meteor I’d recommend you first follow the TODO app tutorial on the official Meteor site.

You can find the code for the completed app in the accompanying GitHub repo.

Creating the app

If you don’t have Meteor installed you should follow the instructions on their site according to your OS.

Generate the Scaffolding

Now with Meteor installed, open your terminal and run the following command:

meteor create TicTacToe-Tutorial

This will create a folder with the name of your app (in this case TicTacToe-Tutorial). This new folder contains the basic file structure for an app. There’s actually a sample app inside.

Navigate to the folder:

cd TicTacToe-Tutorial

And now run the app:

meteor

I know, I know… that’s a terribly hard-to-remember command, and you’ll be using it a lot, so you should start memorizing it!

If everything went fine now the console should be building the app. After it’s done, open your web browser and go to http://localhost:3000 to see the app running. If you have never done so before, I’d recommend you play around with the sample app. Try to figure out how it works.

Let’s take a look at the file structure. Open your app’s folder. The only things there that we care about (for now) are the client folder and the server folder. The files inside the client folder will be downloaded and executed by the client. The files in the server folder will only be executed on the server and the client has no access to them.

These are the contents in your new folder:

client/main.js        # a JavaScript entry point loaded on the client
client/main.html      # an HTML file that defines view templates
client/main.css       # a CSS file to define your app's styles
server/main.js        # a JavaScript entry point loaded on the server
package.json          # a control file for installing NPM packages
.meteor               # internal Meteor files
.gitignore            # a control file for git

Building the board

A TicTacToe board is a simple three by three table; nothing too fancy, which is great for our first multiplayer game, so we can focus on the functionality.

The board will be downloaded by the client, so we’ll be editing files inside the client folder. let’s begin by deleting the contents on main.html and replacing it with the following:

client/main.html

<head>
  <title>tic-tac-toe</title>
</head>

<body>
  <table id="board">
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
  </table>
</body>

Don’t forget to save your files after making changes! Otherwise, they won’t be acknowledged by Meteor.

Now let’s add some css to our board. Open the main.css file and add the following content:

client/main.css

table
{
  margin: auto;
  font-family: arial;
}

.field
{
  height: 200px;
  width: 200px;
  background-color: lightgrey;
  overflow: hidden;
}

#ui
{
  text-align: center;
}

#play-btn
{
  width: 100px;
  height: 50px;
  font-size: 25px;
}

.mark
{
  text-align: center;
  font-size: 150px;
  overflow: hidden;
  padding: 0px;
  margin: 0px;
}

.selectableField
{
  text-align: center;
  height: 200px;
  width: 200px;
  padding: 0px;
  margin: 0px;
}

We’ve also added a few extra ids and classes that we’ll be using later on in this tutorial.

Finally, delete client/main.js, as we won’t be needing it, and open the app in the browser to see how it looks.

This is fine and all, but is not an optimal solution. Let’s do some refactoring by introducing Blaze Templates.

Creating a Template

Templates are pieces of HTML code with their own functionality that you can reuse anywhere in your app. This is a great way to break up your apps into reusable components.

Before creating our first template, we’ll add two more folders inside the client folder. We’ll call one html and the other one js.

Inside the html folder, create a new board.html file with the following content:

client/html/board.html

<template name="board">
  <table id="board">
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
  </table>
</template>

Now, on the main.html folder replace the content inside the body tag with the following code:

client/main.html

<head>
  <title>tic-tac-toe</title>
</head>

<body>
  {{>board}}
</body>

This will insert our template with the property name="board", inside the body tag.

But this is the same hard coded board that we had before. Only now, it’s inside a template, so let’s take advantage of the template helpers to build our board dynamically.

Using helpers

We’ll declare a helper in the board template that will provide us with an array with the same length as the dimensions we want our board to have.

inside the js folder create a file called board.js with the following content:

client/js/board.js

import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';

Template.board.helpers({
  sideLength: () => {
    let side = new Array(3);
    side.fill(0);

    return side;
  }
});

Now, we’ll use this helper in the board’s template HTML to repeat one single row for each element in the array provided by the helper. To help us with this, we’ll use the Each-in Spacebars block helper.

Replace the content inside the board.html file with the following:

client/html/board.html

<template name="board">
  <table id="board">
    {{#each sideLength}}
      {{#let rowIndex=@index}}
      <tr>
        {{#each sideLength}}
        <td class="field" id="{{rowIndex}}{{@index}}">
          {{{isMarked rowIndex @index}}}
        </td>
        {{/each}}
      </tr>
      {{/let}}
    {{/each}}
  </table>
</template>

Notice that we’re looping through the array twice, once for the rows and once for the columns, instantiating the corresponding tag (tr or td) as we go. We’re also setting their id properties as the @index of the row + @index of the column. What we get is a two digits number that will help us identify that element, with its position on the board.

Check out the app at http://localhost:3000 to see how it’s looking so far.

UI

Now that we have a good looking board, we’ll need a play button and a tag to show information on the current game.

Let’s begin by creating the ui.html file inside the html folder… you know the drill. Now, add the following content to it:

client/html/ui.html

<template name ="ui">
  <div id="ui">
    {{#if inGame}}
      <p id="status">
      {{status}}
      </p>
    {{else}}
      <button id="play-btn">Play</button>
    {{/if}}
  </div>
</template>

As you can see we’re using the #if Spacebars block helper and the inGame helper (that we haven’t yet defined) as a condition. There’s the status helper inside the p tag too. We’ll define that later also.

How does it work? #if the inGame helper returns true, the player will see whatever’s in the status helper. Otherwise, we’ll simply show the play button.

Don’t forget, for this component to be displayed we need to add it to our main client template:

client/main.html

<head>
  <title>tic-tac-toe</title>
</head>

<body>
  {{>ui}}
  {{>board}}
</body>

Logging in

We won’t be dealing with any login UI. We will install a very useful package called brettle:accounts-anonymous-auto that will automatically log in all users anonymously into our app.

Head over to your console and run the following command:

meteor add brettle:accounts-anonymous-auto

Now, when you open the app for the first time after adding this package, it’ll create a new user, and every time you open the app on the same browser it’ll remember you. If we’re not keeping any data from said user, it might be better to just remove them when they log out. But we’re not going over that in this tutorial.

Building the Game

Finally, we’re going to start building the game itself! Let’s go over the functionality we’ll be implementing, to have a clear view of what’s coming next.

We’ll need functionality for:

  • Creating a game
  • Joining an existing game
  • Making a move
  • Establishing win conditions
  • Showing game status to players
  • Destroying a finished game instance

To take advantage of Meteor’s Latency Compensation we’ll put most of this code in a place accessible by both the client and the server.

To achieve this we’ll create a folder called lib at the root of our project. Whatever we put in there will be downloaded by the client so we have to be very cautious. You don’t want to be giving any API keys or access to hidden functionality to the client by accident.

Games Collection

Meteor uses Mongo Collections. If you’re not very familiar with Mongo, but you’ve used any other document oriented database you’ll be fine. Otherwise, think of collections as tables, where every row is independent of the next. One row can have six columns, while another row in the same table can have four completely different columns.

We need to create a collection and we need it to be accessible to both the client and the server. So we will create a games.js file inside the lib folder and there we’ll create an instance of a collection called “games” and store it in a global variable, Games:

lib/games.js

import { Mongo } from 'meteor/mongo';

Games = new Mongo.Collection("games");

By now, you’re probably wondering why we are giving the player access to the database and the game logic. Well, we’re only giving local access to the player. Meteor provides the client with a local mini mongo database that we can only populate with a Publish-Subscribe pattern as I’ll show you in a little bit. That’s the only thing the client has access to. And even if clients write to their local database, if the information does not match whatever’s on the server’s database, it’ll be overridden.

That said, Meteor comes by default with a couple of very insecure packages installed. One is called autopublish, it automatically publishes all of your collections and subscribes the client. The other one is called insecure and it gives the client write access to the database.

Both of these packages are great for prototyping, but we should go ahead and uninstall them right now. Go to the console and run the following command:

meteor remove insecure
meteor remove autopublish

With that out of the way, now we need a way to synchronize what we do in the client with what we do on the server. Enter Meteor Methods.

games.play Method

Meteor.methods is an object where we can register methods that can be called by the client with the Meteor.call function. They will be executed, first on the client and then on the server. So clients will be able to see changes happen instantly thanks to the local Mongo database. Then the server will run the same code on the main database.

Let’s create an empty games.play method below the games collection:

lib/games.js

Meteor.methods({
  "games.play"() {

  }
});

Creating a game

Create a file in the lib folder called gameLogic.js and in it we’ll create the GameLogic class with a newGame method, where we’ll insert a new document into our games collection:

lib/gameLogic.js

class GameLogic
{
  newGame() {
    if(!this.userIsAlreadyPlaying()) {
      Games.insert({
        player1: Meteor.userId(),
        player2: "",
        moves: [],
        status: "waiting",
        result: ""
      });
    }
  }
}

In this piece of code, we’re asking if the player is already playing before we insert a new game, since we’re not going to support more than one game at a time for each player. This is a very important step, otherwise we might end up facing a huge bug.

Let’s add the userIsAlreadyPlaying method below newGame():

lib/gameLogic.js

userIsAlreadyPlaying() {
  const game = Games.findOne({$or:[
    {player1: Meteor.userId()},
    {player2: Meteor.userId()}]
  });

  if(game !== undefined)
    return true;

  return false;
}

Let’s go over the process of starting a new game.

When a player hits the play button, we’ll look for an existing game to join them to. If said player can’t find a game to join, a new game will be created. In our model, player1 is the player who created the game, player2 is an empty string and status is by default “waiting”.

So, if another player hits the play button, they’ll look for a game with an empty player2 field and a status field with the value “waiting”. Then we’ll set that player as player2 and change the status accordingly.

Now we have to make our GameLogic class accessible by the Meteor methods inside games.js. We’ll export an instance of our class and then import it in the games.js file. Add this line at the bottom of the gameLogic.js file, outside the class:

export const gameLogic = new GameLogic();

Add the following line at the top of the games.js file:

import { gameLogic } from './gameLogic.js';

Now we can add logic to our empty games.play() method. First we look for a game with the status: “waiting” and then we call newGame() if no other game was found:

lib/games.js

Meteor.methods({
  "games.play"() {
    const game = Games.findOne({status: "waiting"});

    if(game === undefined) {
      gameLogic.newGame();
    }
  }
});

Publications

In order to find a game, we’ll need to give the client access to the games collection. To do this, we’ll create a Publication. Publications let us show clients, only the data we want them to see. Then we Subscribe clients to a Publication in order to give them access to that data.

To give players access to the games collection, we’ll create a ‘Games’ Publication. But when players are added to a new game, we’ll give them access to all of the fields in that particular game. So there’s also going to be a ‘My game’ Publication.

Go to the main.js file inside the server folder and replace it’s contents with the following:

server/main.js

import { Meteor } from 'meteor/meteor';

Meteor.publish('Games', function gamesPublication() {
  return Games.find({status: "waiting"}, {
    fields:{
      "status": 1,
      "player1": 1,
      "player2": 1
    }
  });
});

Meteor.publish('MyGame', function myGamePublication() {
  return Games.find({$or:[
      {player1: this.userId},
      {player2: this.userId}]
    });
});

Now we need to Subscribe to the ‘Games’ publication. We’ll do that in the UI Template’s onCreated method callback.

Create a ui.js file in client/js/ with the following code:

import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';

Template.ui.onCreated(() => {
  Meteor.subscribe('Games');
});

Play Event

Templates provide an events object where we can register…. guess what? Bingo! Events. We’ll create an event in the UI template. Whenever a player clicks a DOM element with the ID ‘play-btn’ we’ll set a session variable inGame to true, we’ll call the games.play method, and subscribe to the MyGame collection.

Session variables can be used anywhere in the client code, even from template to template. To use them we’ll need to add the Session package:

meteor add session

Head over to the ui.js file and add the following lines after the onCreated method:

client/js/ui.js

Template.ui.events({
  "click #play-btn": () => {
    Session.set("inGame", true);
    Meteor.call("games.play");
    Meteor.subscribe('MyGame');
  }
});

It’s good practice to import the packages we’re using in each file. Since we’re using the Session package in the ui.js file we should import it. Just add the following line at the top:

import { Session } from 'meteor/session';

Good! Now we need to add a couple of helpers. Remember, ui.html? Give it a quick look. We used an inGame helper and a status helper. let’s declare them below the events object:

client/js/ui.js

Template.ui.helpers({
  inGame: () => {
    return Session.get("inGame");
  },
  status: () => {

  }
});

As you can see, the inGame helper returns the value stored in the inGame session variable. We’ll leave the status helper empty for now.

Joining a game

After all, you’ve done so far, joining a game should be pretty straight forward.

First we’ll add the joinGame method to the GameLogic class:

lib/gameLogic.js

joinGame(game) {
  if(game.player2 === "" && Meteor.userId() !== undefined) {
    Games.update(
      {_id: game._id},
      {$set: {
        "player2": Meteor.userId(),
        "status": game.player1
        }
      }
    );      
  }
}

As you can see, we pass on a game variable and we set the player2 field to the player’s _id, and the status field to the _id_ of player1. This is how we’ll know whose turn it is.

Now we’ll call this method from games.play(). Go to the games.js file and replace the content of the games.play method with the following:

lib/games.js

Meteor.methods({
  "games.play"() {
    const game = Games.findOne({status: "waiting"});

    if(game === undefined) {
      gameLogic.newGame();
    } else if(game !== undefined && game.player1 !== this.userId && game.player2 === "") {
      gameLogic.joinGame(game);
    }
  }
});

So now, we added an else if with three conditions: if we found a game and player1 is not this player and player2 is an empty string, we join the game.

Making a move – Logic

When we defined our model for every new game, we declared a moves field with an empty array ([]) as the default value. A move will be a JSON object composed by the _id of the player who made the move and the position selected.

Head to the games.js file and add the following method below games.play(). Remember, Meteor.methods takes a JSON object, so methods should be separated by commas:

lib/games.js

"games.makeMove"(position) {
  check(position, String);

  gameLogic.validatePosition(position);

  let game = Games.findOne({status: this.userId});

  if(game !== undefined) {
    gameLogic.addNewMove(position);

    if(gameLogic.checkIfGameWasWon()) {
      gameLogic.setGameResult(game._id, this.userId);
    } else {
      if(game.moves.length === 8) {
        gameLogic.setGameResult(game._id, "tie");
      } else {
        gameLogic.updateTurn(game);
      }
    }
  }
}

Let’s go over this method line by line. It takes a string position as a parameter. First, we use the check package to make sure what we received is a string and not some malicious code that could harm our server and then we validate the position.

After that, we find a game in which the status field is the same as the _id of the player making the move; this way we know it’s their turn. If we found that game or, in other words, if it’s that player’s turn, we’ll add the move to our moves array. Then we check if the game was won after that move. If it was indeed won, then we’ll set the current player as the winner. Otherwise, if it was not won, but there are already eight moves in the array, then we declare a tie. If there are not eight moves yet, we update the turn to let the next player move.

Just like we did with the Session package in the ui.js file. We should import the check package in the games.js file. You know how it goes… add the following line at the top.

import { check } from 'meteor/check';

We’re using a bunch of methods from the GameLogic class that we haven’t defined yet. So, let’s go ahead and do that.

Go to gameLogic.js and add the following methods in the GameLogic class:

validatePosition()

validatePosition(position) {
  for (let x = 0; x < 3; x++) {
    for (let y = 0; y < 3; y++) {
      if (position === x + '' + y)
        return true;
    }
  }

  throw new Meteor.Error('invalid-position', "Selected position does not exist... please stop trying to hack the game!!");
}

Here we simply move through a 3×3 grid to make sure the position sent is within its limits. If we can’t find the position sent by the client, in the grid, we throw an error.

addNewMove()

addNewMove(position) {
  Games.update(
    {status: Meteor.userId()},
    {
      $push: {
        moves: {playerID: Meteor.userId(), move: position}
      }
    }
  );
}

Here we use the $push Mongo operator to, ahem, push the new move, containing the current player _id and the position, into the array.

setGameResult()

setGameResult(gameId, result) {
  Games.update(
    {_id: gameId},
    {
      $set: {
        "result": result,
        "status": "end"
      }
    }
  );
}

Using the $set operator again, we update the result field to the value of the result parameter which can either be the _id of one of the players or ‘tie’, and we set the status to ‘end’.

updateTurn()

updateTurn(game) {
  let nextPlayer;

  if(game.player1 === Meteor.userId())
    nextPlayer = game.player2;
  else
    nextPlayer = game.player1;

  Games.update(
    {status: Meteor.userId()},
    {
      $set: {
        "status": nextPlayer
      }
    }
  );
}

This one’s fairly straightforward. We take both players as parameters and we figure out which one is the current player, then we set the status field to the other player’s _id.

Winning the game

There’s still one method left to declare from the games.makeMove method; the winning algorithm. There are other, more effective ways of calculating who won in a TicTacToc game, but I decided to go for the most intuitive and simple solution I could think of for this tutorial.

Go to the gameLogic.js file and add the following method in the GameLogic class:

lib/gameLogic.js

checkIfGameWasWon() {
  const game = Games.findOne({status: Meteor.userId()});

  const wins = [
  ['00', '11', '22'],
  ['00', '01', '02'],
  ['10', '11', '12'],
  ['20', '21', '22'],
  ['00', '10', '20'],
  ['01', '11', '21'],
  ['02', '12', '22']
  ];

  let winCounts = [0,0,0,0,0,0,0];

  for(let i = 0; i < game.moves.length; i++) {
    if(game.moves[i].playerID === Meteor.userId()) {
      const move = game.moves[i].move;

      for(let j = 0; j < wins.length; j++) {
        if(wins[j][0] == move || wins[j][1] == move || wins[j][2] == move)
        winCounts[j] ++;
      }
    }
  }

  for(let i = 0; i < winCounts.length; i++) {
    if(winCounts[i] === 3)
      return true;
  }

  return false;
}

Let’s look at this method closely.

First, we find the current game. Then, we declare a matrix with all the possible win combinations and another variable with an array of seven zeroes: one for each combination. After that, we’ll loop through all the moves made by the current player and compare them with every position of each combination. For every coincidence we add 1 to the corresponding winCount index position. If any of the winCount indexes adds up to 3, we’ll know that the current player has won.

Don’t worry if you didn’t get it that first time. Take a small break, have some coffee and read it again later a couple of times with a set of fresh eyes. An explanation of a code can be confusing. Sometimes it’s even better to just read the code and figure out what it does.

Making a move – Controller

Our player controller for this game is nothing more than a simple click. So implementing that should be a piece of cake. Let’s go to the board.js file and add events template object to our file after the helpers:

client/js/board.js

Template.board.events({
  "click .selectableField": (event) => {
    Meteor.call("games.makeMove", event.target.id);
  }
});

Simple, right? When the player clicks a DOM element with the class ‘selectableField’, we call the games.makeMove method, passing the id of the DOM element as the position parameter. Remember we’re naming the id after the element’s position in the grid. Take a look at the board.html file to refresh your memory if you need to.

Showing moves

Now, in the same file, we’ll create a helper called isMarked, that will switch between mark and selectableFields. This way we’ll be able to see which positions have been selected and let empty positions be selected.

Add this helper below the sideLength helper:

client/js/board.js

isMarked: (x, y) => {
  if(Session.get("inGame")) {
    let myGame = Games.findOne();

    if(myGame !== undefined && myGame.status !== "waiting") {
      for(let i = 0; i < myGame.moves.length; i++) {
        if(myGame.moves[i].move === x + '' + y) {
          if(myGame.moves[i].playerID === Meteor.userId())
            return "<p class='mark'>X</p>";
          else
            return "<p class='mark'>O</p>";
        }
      }
      if(myGame.status === Meteor.userId())
        return "<div class='selectableField' id='"+x+y+"'></div>";
    }
  }
}

and add the helper to template:

client/html/board.html

...
<td class="field" id="{{rowIndex}}{{@index}}">
  {{{isMarked rowIndex @index}}}
</td>
...

Let’s go over this function. We take a row and a column as parameters (x, y). If we’re inGame, we look for that game. If we find it and the status is ‘waiting’, we loop through all of the moves and if the given row + column match one of our moves, we’ll draw an X on the board. If it matches one of the other player’s moves we’ll draw an O.

Our moves will always be an X and our opponent’s an O, in every game. Although, your opponents will see their moves drawn as an X. We don’t really care who’s got the X or the O since we’re playing on different devices, maybe even in different countries. What matters here is that each player knows which are their moves and which their opponents’.

Showing Status

We’re almost done! Remember the empty status helper in the ui.js file? Populate it with the following code:

client/js/ui.js

status: () => {
  if(Session.get("inGame")) {
    let myGame = Games.findOne();

    if(myGame.status === "waiting")
      return "Looking for an opponent...";
    else if(myGame.status === Meteor.userId())
      return "Your turn";
    else if(myGame.status !== Meteor.userId() && myGame.status !== "end")
      return "opponent's turn";
    else if(myGame.result === Meteor.userId())
      return "You won!";
    else if(myGame.status === "end" && myGame.result !== Meteor.userId() && myGame.result !== "tie")
      return "You lost!";
    else if(myGame.result === "tie")
      return "It's a tie";
    else
      return "";
  }
}

This one’s pretty obvious but I’ll explain it just in case. If we’re inGame, we look for the current game. If the status equals ‘waiting’, we tell the player to wait for an opponent. If status equals the player’s _id, we tell them it’s their turn. If status is not their _id and the match isn’t finished, we tell them it’s the opponent’s turn. If the result equals the player’s _id, we tell the player that they’ve won. If the match came to an end, and the result is not their _id and it’s not a “tie”, then they lost. If the result equals “tie”, we tell them that it’s a tie… duh! ;)

As it is now, you can take it for a spin. Yes! Go ahead open a normal browser window and a private tab and play against yourself. Try not to have too much fun though or you’ll end up alone for the rest of your life (it’s true I swear).

Logging out

Buuuuuut, we’re not finished yet. Nope! What if we disconnect and leave other players by themselves? What about all those completed games filling precious space in our database? We need to track the player’s connection and act accordingly.

But first we’ll need a way to remove games and remove players from games. Go to gamesLogic.js and add the following methods in the GameLogic class:

lib/gameLogic.js

removeGame(gameId) {
  Games.remove({_id: gameId});
}

removePlayer(gameId, player) {
  Games.update({_id: gameId}, {$set:{[player]: ""}});
}

The removeGame method takes a gameId as argument and removes it. removePlayer() takes a gameId and a player (a string that can either be player1 or player2) as arguments and empties that player’s field in that particular game.

To track the user’s connection, we’ll install a useful package called mizzao:user-status. Go to the console, close the running app with ctrl+c and run the following command:

meteor add mizzao:user-status

This package has a connectionLogout callback that provides a parameter with important information like the userId of the disconnecting user.

Go to the main.js file in the server folder, and add the following callback at the bottom.

/server/main.js

UserStatus.events.on("connectionLogout", (fields) => {
  const game = Games.findOne(
  {$or:[
    {player1: fields.userId},
    {player2: fields.userId}]
  });

  if(game != undefined) {
    if(game.status !== "waiting" && game.status !== "end") {
      if(game.player1 === fields.userId) {
        gameLogic.setGameResult(game._id, game.player2);
        gameLogic.removePlayer(game._id, "player1");
      } else if(game.player2 === fields.userId) {
        gameLogic.setGameResult(game._id, game.player1);
        gameLogic.removePlayer(game._id, "player2");
      }
    } else {
      if(game.player1 === "" || game.player2 === "") {
        gameLogic.removeGame(game._id);
      } else {
        if(game.player1 === fields.userId)
          gameLogic.removePlayer(game._id, "player1");
        else if(game.player2 === fields.userId)
          gameLogic.removePlayer(game._id, "player2");
      }
    } 
  }
});

So, if we can find a game where the disconnected player is either player1 or player2, we check if the status of that game is not “waiting” and the game hasn’t come to an end. If it has, we give the victory to the opponent and remove the disconnecting player. Otherwise, we either remove the game (if any of the player fields are empty) or. if that’s not the case, we remove the disconnecting player from the game.

Like we did with the other packages, we should import the UserStatus package. We also used some methods from the GameLogic class in the connectionLogout callback, so go ahead and import both of them at the top of the server/main.js file:

import { UserStatus } from 'meteor/mizzao:user-status';
import { gameLogic } from '../lib/gameLogic.js';

Wrapping up

Finally, you should have a working game! As it is, you can upload it and try it out with your friends… or by yourself.

If any of the things we’ve done make little-to-no sense to you just now, don’t worry about it; It’ll make sense soon enough if you keep studying the code. You just need some time to wrap your head around some concepts. That’s a completely natural process. If you get stuck, don’t forget to check out the code for the completed app.

When you feel comfortable enough with the code, you should start trying to add some functionality. Maybe implement a different winning algorithm that might let you increase the board’s size. Perhaps implement persistence for players in order to save stats and keep records of games. You could even implement a login interface and let players choose a user name. What about challenging a friend? And of course, you could also use the same concepts to create an entirely different game.

I’d love to see what you come up with, so please let me know! I hope you enjoyed this tutorial, leave your doubts and comments down in the comments. I’ll see you in the next one!

Frequently Asked Questions (FAQs) about Building a Multiplayer TicTacToe Game with Meteor

How can I add more players to my TicTacToe game?

Adding more players to your TicTacToe game involves modifying the game logic to accommodate more than two players. This can be achieved by creating an array of players and assigning each player a unique identifier. The game logic should then be adjusted to cycle through the array of players, allowing each one to make a move in turn. Remember to also adjust the win condition to check for a winner after each player’s move.

Can I customize the game board in the TicTacToe game?

Yes, you can customize the game board in your TicTacToe game. This can be done by adjusting the CSS properties of the game board in your code. You can change the color, size, and shape of the cells, as well as the overall layout of the board. However, keep in mind that any changes to the board size or shape may require adjustments to the game logic.

How can I add a chat feature to my TicTacToe game?

Adding a chat feature to your TicTacToe game can enhance the multiplayer experience. This can be achieved by using Meteor’s built-in methods for real-time data updates. You can create a new collection for chat messages and use Meteor’s publish and subscribe methods to send and receive messages in real-time. Remember to add a user interface for the chat feature in your game’s HTML and CSS.

Can I make my TicTacToe game mobile-friendly?

Yes, you can make your TicTacToe game mobile-friendly. This involves making your game responsive so that it adjusts to different screen sizes. You can achieve this by using responsive design techniques in your CSS, such as media queries. You may also need to adjust the game controls to be touch-friendly for mobile users.

How can I add sound effects to my TicTacToe game?

Adding sound effects to your TicTacToe game can make it more engaging. This can be done by using the HTML5 audio API. You can create new audio objects for each sound effect and play them at the appropriate times in your game logic. Remember to include controls for muting or adjusting the volume of the sound effects.

Can I add AI opponents to my TicTacToe game?

Yes, you can add AI opponents to your TicTacToe game. This involves creating a computer player that can make moves based on the current state of the game board. You can use various AI algorithms for this, such as the Minimax algorithm, which is commonly used in TicTacToe games.

How can I host my TicTacToe game online?

Hosting your TicTacToe game online allows others to play it. This can be done by deploying your game to a web server. Meteor provides built-in deployment tools that you can use to host your game on a Meteor server. Alternatively, you can use other hosting services that support Node.js applications.

Can I monetize my TicTacToe game?

Yes, you can monetize your TicTacToe game. There are several ways to do this, such as through in-game advertisements, in-app purchases, or by charging for the game itself. However, keep in mind that monetizing your game may require compliance with certain legal and financial regulations.

How can I improve the performance of my TicTacToe game?

Improving the performance of your TicTacToe game can provide a better user experience. This can be achieved by optimizing your code, reducing the size of your assets, and using efficient data structures and algorithms. You can also use Meteor’s built-in tools for performance profiling and debugging.

Can I add multiplayer support to other types of games using Meteor?

Yes, you can add multiplayer support to other types of games using Meteor. The principles of real-time data updates, user authentication, and client-server communication that you learned in building a TicTacToe game can be applied to other types of games as well. Meteor’s flexibility and scalability make it a great choice for building a wide range of multiplayer games.

BeardscriptBeardscript
View Author

Aside from being a software developer, I am also a massage therapist, an enthusiastic musician, and a hobbyist fiction writer. I love traveling, watching good quality TV shows and, of course, playing video games.

gamesmeteornilsonj
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week