Learn How to Build a Multiplayer Tic Tac Toe (1)

No Comments

Do you want to build a multiplayer game and learn how to get started? In this post, we will learn how to create a tic tac toe game that you can play with your friends!

In this post, we will go over the backend creation of the game and in the next part, we will create the visuals for it. The final result will be a basic tic tac toe experience that may get extended with a room system in a later post. You can find all resources for this project on GitHub.

Here is a little preview of the final result with the backend and the frontend working together.

tic-tac-toe-preview

Let’s start building the multiplayer tic tac toe!

Steps

In this post we will create the backend for the game, therefore we first think about the steps involved in a match of multiplayer tic tac toe and then implement the steps one by one.

  1. Client-Server model
  2. Steps in a match of tic tac toe
  3. Set up the project
  4. Implement the tic tac toe field
  5. Implement the server logic

Client-Server model

client-server-model

In a multiplayer game, there is often a server and multiple clients involved. The server runs the logic and connects the different players with each other. The clients are the players and they send different actions to the server.

In the game of tic tac toe, we have the server storing the field, processing the moves, and handling the connection of the players. In tic tac toe we have a maximum of two players in one game. Because of that, the server has to check if there are already two players before he connects a new one.

Steps in a match of tic tac toe

steps in a game of tic tac toe

In the image, we can see how a match of tic tac toe will go down. First, the two clients connect to the server. Each client receives information about their player id. With this information, we can determine the player’s figure (e.g. cross or circle) and the winner during the match.

Second, the server starts the match and informs the players about who will start the match.

In the third step, the two players are playing against each other. First, the player will click on a field and that information is sent to the server. There it will be processed and the information about the selected field and the next player are sent to the clients by the server. This will be repeated until one player won the game or no free cell is available anymore.

In the last step, the server will inform the players about the outcome of the match. This will also end the match.

With all steps laid out, we can retrieve useful information about the events that the server has to send and receive.

Send (step)

Recieve (step)

Set up the project

We will implement the logic for our multiplayer tic tac toe with the socket.io library. The socket.io library helps us to work with WebSockets more easily. A WebSocket is basically a server that you can join with the ability to send and receive messages. To start the project we create a folder for our backend and initialize the project with:

npm init

After the initialization, we install the socket.io library with the following command:

npm i socket.io

When we successfully installed the library we will create a folder for the components and in there create the file field.js. This file will contain the tic tac toe field. Additionally, we create the index.js file in our root directory. With this in place we have the following structure:

tic-tac-toe-backend-file-structure

Implement the tic tac toe field

To implement the tic tac toe field we first think about the different aspects the field needs.

  1. field with 9 cells (2d array)
  2. set and get cell values
  3. check for the win in a row, col and diag
  4. check for game over
  5. reset and get the field

With this information, we will create a class, in the field.js file.

class Field {
    /** 
     * current state of the field
     * 0 = none
     * 1 = player 1
     * 2 = player 2
     */
    field = [[0,0,0],[0,0,0],[0,0,0]];
}

In the next steps, we extend the class with the helper methods discussed before. We will start with a simple implementation of the cell getter and setters.

getCell(x, y) {
    return this.field[y][x];
}

setCell(x, y, value) {
    this.field[y][x] = value;
}

Next, we will implement a way to check if a certain player id has won. Therefore we create three sub-functions to check the columns, the rows, and the diagonals for the player’s id. With this functions we can easily check the different possibilities for a win.

checkCol(colIdx, id) {
    return this.field[colIdx].every((cell) => cell == id);
}

checkRow(rowIdx, id) {
    let row = [];
    this.field.forEach((col) => row.push(col[rowIdx]));
    return row.every((cell) => cell == id);
}

checkDiagonal(lr, id) {
    let lrIdxes = lr ? [0,1,2] : [2,1,0];
    let lrIdx = 0;
    let diag = []
    this.field.forEach((col) =>{ diag.push(col[lrIdxes[lrIdx]]); lrIdx++});
    return diag.every((cell) => cell == id);
}

checkWin(id) {
    let colWin = this.checkCol(0, id) || this.checkCol(1, id) || this.checkCol(2, id);
    let rowWin = this.checkRow(0, id) || this.checkRow(1, id) || this.checkRow(2, id);
    let diagWin = this.checkDiagonal(true, id) || this.checkDiagonal(false, id);

    return colWin || rowWin || diagWin;
}

Now we can check if a player has won, but there is always the possibility for a draw. That’s why the next function will determine between a win or a draw. In case we have a winner it will return a game over and the winner’s id.

checkGameOver(id) {
    let fieldFull = this.field[0].every((cell) => cell != 0) &&
checkGameOver(id) {
    let fieldFull = this.field[0].every((cell) => cell != 0) &&
     this.field[1].every((cell) => cell != 0) &&
     this.field[2].every((cell) => cell != 0);
    let win = this.checkWin(id)

    return {"over": fieldFull || win, "id": win ? id : 0};
}

The last functions will recieve the current game state or reset the field for a new game.

resetField() {
    this.field = [[0,0,0],[0,0,0],[0,0,0]];
}

getField() {
    return this.field;
}

Now we implemented all functions needed by the field class to run a match of tic tac toe, to recieve a winner and also to start a new match.

Implement the server logic

We will implement the server logic in the index.js file and start by importing the needed packages.

// add cors https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
const options = {cors: true};
const io = require("socket.io")(options);
const {Field} = require("./components/field");

To import the field class we have to add an export in the field.js file.

module.exports.Field = Field;

In the next step, we will create some variables to keep track of the current state of the match. First, we create the field to use during the match. Then we have an object with the player id and a string that later contains the socket id. With this variable, we will enable the possibility to reconnect and still be the same player. The last three variables hold information about the start, active player, and game-over state.

const field = new Field();
let players = { 1: "", 2: "" };
let started = false;
let activePlayer = 1;
let gameOver = false;

Now we will create the WebSocket and implement the connection event. The event will fire for each new connection and will hold all of the send and receive events. The first step in the connection is to check if a player is allowed to join because in a game of tic tac toe only two players are allowed, every other player who tries to join will get disconnected. If we don’t have two players already we will add the new player to the player’s object and give him his id (1 or 2).

// event gets called when someone connects to the server
io.on("connection", socket => {
    // disconnect if 2 clients connected
    if (io.sockets.sockets.size > 2) {
        console.log("Something went wrong! To many players tried to connect!");
        socket.disconnect();
    }

    // join server with socket id
    const sockId = socket.id;
    joinPlayers(sockId)

    // get player id (1,2)
    const id = getKeyByValue(players, sockId);
    socket.emit('clientId', id);
}

io.listen(3000);
console.log("Sever listening on port 3000!")

// add socket id to player obj
function joinPlayers(clientId) {
    for (const keyIdx in players) {
        let curr = players[keyIdx];
        if (curr == "") {
            players[keyIdx] = clientId;
            return;
        }
    }
}

function getKeyByValue(obj, value) {
    return Object.keys(obj).find(key => obj[key] === value);
}

The next step is to start the match and to continue the match on a reconnect. Therefore, the implementation will check if two players are connected and start the game if this is the case. But if the match already started we send the new player his id and the current state of the match.

// start the game when 2 players connect
if (io.sockets.sockets.size == 2 && !started) {
    started = true;
    io.emit('start', activePlayer);
    console.log("Match started");
}

// send out the current state of the field + active id to continue game
if (started) {
    socket.emit('continue', activePlayer, field.getField());
}

Now we implement the turn functionality. In case of a turn we have to listen for events of the players and then use the data to update the field, inform the players and then check if the game is over. If the game is over we also reset the state variables and disconnect from the server on the client side (this will be implemented in the next part).

// player turn is send to server
socket.on("turn", (turn) => {
    console.log(`Turn by ${id}: ${turn.x}, ${turn.y}`);
    if (gameOver) return;

    // switch activePlayer
    activePlayer = 3 - activePlayer;

    // set the field
    field.setCell(turn.x, turn.y, id);

    // notify all clients that turn happend and over the next active id
    io.emit('turn', {
        "x": turn.x,
        "y": turn.y,
        "next": activePlayer
    });

    // check if game over
    overObj = field.checkGameOver(id);
    gameOver = overObj['over'];
    if (gameOver) {
        console.log(overObj['id'] != 0 ? `Game over! The winner is player ${id}`
            : `Game over! Draw`);
        io.emit('over', overObj);

        // reset game
        field.resetField();
        started = false;
        gameOver = false;
    }
});

The last step is to remove the players from the player object during the disconnect event.

// remove socket id from player object
socket.on("disconnect", () => {
    let key = getKeyByValue(players, socket.id)
    players[key] = "";
})

With all of this implemented, we have done everything needed for the backend of the simple multiplayer tic tac toe. In the next part, we will implement the front end for the game. On my GitHub, you can find all of the files for the project.

If you have any questions or feedback feel free to leave a comment or send me an email at mail@programonaut.com.

The icons in this post are mainly created by Freepik from www.flaticon.com.

Discussion (0)

Add Comment

Your email address will not be published.