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
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.
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.