Codementor Events

Build a Multi-user App using Socket.io (Part 2): Creating a Matchmaking Game Server

Published Jan 18, 2017
Build a Multi-user App using Socket.io (Part 2): Creating a Matchmaking Game Server

Welcome to part two! Be sure to review and read through part one here, else face the wrath of ambiguous confusion! (You have been forewarned). Today we intend to wrap up the engine and achieve the following objectives:

  • Force only one (1) game per user
  • Allow a user to join a game (be matched to another user's game and be included)
  • Allow a user to leave a game they have joined, both through exiting the page and through a button.

Bonus: Bugs also happen during development. Since forking and modifying the existing socket.io chat application, I've noticed that our front-facing screen has an error in the markup. There seems to be an element below the screen that pushes the chat window above the screen — it looks awful. Before finishing up, we will troubleshoot and fix this bug so that the chat input field stays consistently at the bottom of the screen. We have much ground to cover so let's get moving.

Getting started

Open the project folder that you collected from the repo (or built along with me from forking the original socket.io chat application OR  using my own GitHub repo (as built by the end of part one) for reference).

Modifying the client-side file to listen for emitted events from the server file

The first thing we want to do is to modify the main.js (client-side JavaScript file) to listen for emitted events from the server file (index.js). Copy and modify socket.on('gameCreated'). We're going to create a new event called alreadyJoined to replace gameCreated in your copy and get rid of the console.log — at this point the logging() function pushes out messages to the front-end so we shall reuse this code to handle "Join Game" and "Destroy Game" announcements. Next, replace the log message with a more appropriate indicator for our users. I've decided to emit the following:

'You are already in an Existing Game: ' + data.gameId

Got it? Good. Now copy this modified alreadyJoined event and change the event name to joinSuccess. Again, as before, change the logging text to a more appropriate description. Continue by making a copy of the sendGame function, replace the name and emitter name (I've called mine joinGame).

Finally, the joinGame function and pairing socket.on listener. Change the event names to leaveGame and the text log string to reflect the new action. Together, both the function and events above when combined should look like this in your main.js file:


//Join into an Existing Game
function joinGame(){
socket.emit('joinGame');
};

socket.on('joinSuccess', function (data) {
log('Joining the following game: ' + data.gameId);
});


//Response from Server on existing User found in a game
socket.on('alreadyJoined', function (data) {
log('You are already in an Existing Game: ' + data.gameId);
});


function leaveGame(){
socket.emit('leaveGame');
};

socket.on('leftGame', function (data) {
log('Leaving Game ' + data.gameId);
});

Adding buttons

Let's add in the "Join" and "Leave Game" buttons as well as fix the issue with the bottom of the screen clipping:

multi-user app

Uncomment the previous joinGame button that we created within the index.html file. Using InspectElement (in Chrome or Firefox), we can observe that the buttons we have added have pushed the elements downward. To address this, we can position the buttons such that they sit in an absolute position. Wherever we choose to place them doesn't matter, the key takeaway is that we need to change the position attribute on the CSS — by default it is set to relative, which is why the elements are being pushed downward in the screenshot. I suggest we change it to absolute positioning, just like the setup of the chat input box. Since we set the buttons into a parent div, let's modify that parent to have absolute positioning, like so, within our style.css:


.gameButtons {

position: absolute;
}

Note: Feel free to also include .joinGame after .createGame on line 18 in the style.css so that the added button matches the CSS look. Let's also shrink the buttons font-size down from 43px to 20px.

Oh but we now have one small new problem, our buttons clip the first chat message near the top of our window:

multi-user app

We have options. One idea which I initially thought was to push the div of the chat area downward:

multi-user app

Though to be honest, I didn't really like the idea of messing with the chatArea element just so the buttons I added in would look proper. I thought that perhaps, we could, instead, just change the CSS float property — until I determined that you cannot apply a float attribute to absolute positioning. No — my approach was to modify the gameButtons CSS by setting Right to 0px (0 also works, just something to specify that this element should position on the right with no (pixel/em/rem/etc.) offset… and moving on with our tutorial:

multi-user app

Validation feature

Let's break from the front-end for a moment and tinker around with the server file, index.js. I propose we first tackle a validation feature that will prevent users from making multiple games. To accomplish this, modify our socket.on('makeGame') snippet on line 91. We are going to scan the gameCollection object to see if a gameObject already exists where socket.username equals our current user and if it does, trigger the alreadyJoined (on line 311 of the main.js file) that we created earlier, because we want to prevent multiple games from being created by the same user — this same mechanism we're building will be duplicated when a user attempts to join into multiple games.

First, let's send a message to the screen via console.log(gameServer) into our makeGame function so we can observe from the server that the gameCollection works as it should… We see that the game count increases, but that the gameID is just being overwritten each pass:

multi-user app

Looking at line 93, it appears that we set the ID to overwrite the data in that position each time, rather than push the data into an array object. As it turns out, for demo purposes, it appeared to function correct prior, but now I see that I had to change a few components to get things working properly.

On line 22, change the Object (squiggly brackets) {} to an array (straight brackets) []. This is so we can append several objects into the same array element. On line 94, add in a new variable for the temporary game Object itself to form — this will allow us to build up the object before appending it to the actual gameCollection array. We are also going to save the specific elements of each gameObject before "pushing" the object into our Game List array. Also be sure to change the old variable names that were being used toward the end of the block that is emitted back out to our front-end. Our modified createGame Block looks like this (be sure to use JSON.stringify so we can actually see the elements within the object when written to screen in that console.log()statement):

//when the client requests to make a Game
socket.on('makeGame', function () {

console.log(JSON.stringify(gameCollection));

var gameObject = {};
gameObject.id = (Math.random()+1).toString(36).slice(2, 18);
gameObject.playerOne = socket.username;
gameCollection.totalGameCount ++;
gameCollection.gameList.push({gameObject});

console.log("Game Created by "+ socket.username + " w/ " + gameObject.id);

io.emit('gameCreated', {
username: socket.username,
gameId: gameObject.id
    });
});

Now, when looking through the console log for the server, we can see each game object is appended to the gameCollection along with our userID for playerOne. Now we can look through the object to see if the player exists — and throw him the error if so. But how to do this???

My first approach I tried to was to use indexOf with the array. However I found that I was not getting traction with selecting any objects, I kept hitting the dreaded "ReferenceError: x is not defined", with "x" being my attempt to dereference down the object chain towards the gameObject.

As a rule of thumb, when the aggravation increases, the use of console.log() should also increase to more quickly unveil the elusive problem causing the said aggravation. (In other words, confirm sanity — I interpret the phrase "sanity check" to mean "Make sure I'm building this right and prove the assertion, for the sake of my sanity!")

Thus the liberal use of console.log() as I went had occurred; I found that gameCollection.gameList[0]['gameObject']['playerOne']; was the correct way to reference all the way down through to the playerOne Name, which was fantastic news. Naturally, I decided to flesh out the game limiter with a FOR loop. Though before I get into the FOR loop, first, add the following on line 93:

var noGamesFound = true;

FOR Loop

Using the FOR loop, we will check to see if playerOne equals socket.username. If a username is found, we will set that noGamesFound variable to false. To wrap things together, we now separate all the code from var gameObject = {} all the way down to the io.emit and wrap that in an IF statement that only executes if our noGamesFound variable equals true. Here is what the updated code should resemble:

//when the client requests to make a Game
socket.on('makeGame', function () {
console.log(JSON.stringify(gameCollection.gameList));

var noGamesFound = true;
for(var i = 0; i < gameCollection.totalGameCount; i++){
var tempName = gameCollection.gameList[i]['gameObject']['playerOne'];
if (tempName == socket.username){
noGamesFound = false;

console.log("This User already has a Game!");

socket.emit('alreadyJoined', {
gameId: gameCollection.gameList[i]['gameObject']['id']});

    }
}

if (noGamesFound == true) {

var gameObject = {};
gameObject.id = (Math.random()+1).toString(36).slice(2, 18);
gameObject.playerOne = socket.username;
gameCollection.totalGameCount ++;
gameCollection.gameList.push({gameObject});

console.log("Game Created by "+ socket.username + " w/ " + gameObject.id);

io.emit('gameCreated', {
username: socket.username,
gameId: gameObject.id
    });
    }
});

Now we attempt to create multiple games and the server rejects them, then throws an indicator message back out towards our user as can be seen here (confirm that the game ids match on both photos):

multi-user app
Front-end view facing our users — showing that they are already in a game.

multi-user app
Console view showing server side with no multiple games generated.

Allowing other players to join another game

With the server now limiting our users to one game per username, we can now proceed to build a functionality that will allow other players to join another game (while checking to confirm they are not already in another game).

Let's quickly go back into client-side JavaScript and configure the joinGame function to fire on the click-handler for the "Join Game" button we added in prior. Copy the createGame click-handler and change it to $joinGame as shown on line 236 of main.js:

$createGame.click(function () {
sendGame();
});

$joinGame.click(function () {
joinGame();
});

What we need now is to build out the actual handler for when the joinGame event is emitted from the client.

At the very bottom of index.js, inside of socket.on('joinGame'), copy the entire contents of the FOR loop from above and place it into the callback function of the joinGame socket handler. Just as before, create a new variable at the top, just above that FOR loop we coped so that it reads, var alreadyInGame = false;.

Take a moment to go back into the actual gameObject builder and add another line to instantiate a player two slots in our game object. Copy the line above and replace playerOne with playerTwo and instead of it being equal to socket.username set it equal to "null" _(when our users create a game, player two is always going to be empty, this allows us to cleanly set a placeholder for the game object in advance).

Okay, now, go back to that copied FOR loop at the bottom of index.js inside the socket.on('joinGame'). Change tempName to plyr1Tmp. Copy this line and replace plyr1Tmp with plyr2Tmp; also replace playerOne with playerTwo on the end of the same line. On the next line, replace tempName with :

plyr1Tmp == socket.username || plyr2Tmp == socket.username

(Using || means OR) We're saying "IF plyr1Tmp equals socket.username OR if plyr2Tmp equals socket.username, Do something..."

On the next line, replace noGamesFound = false with the new alreadyInGame variable such that it reads, alreadyInGame = true;. Conversely, on line 149, change both the variable name and value such that: if (alreadyInGame == false)….

Let's take a break and confirm that things work as expected. For now, my joinGame function looks like this:

socket.on('joinGame', function (){
console.log(socket.username + "wants to join a game");

var alreadyInGame = false;

for(var i = 0; i < gameCollection.totalGameCount; i++){
    var plyr1Tmp = gameCollection.gameList[i]['gameObject']['playerOne'];
    var plyr2Tmp = gameCollection.gameList[i]['gameObject']['playerTwo'];
    if (plyr1Tmp == socket.username || plyr2Tmp == socket.username){

        console.log(socket.username + " already has a Game!");

        socket.emit('alreadyJoined', {
        gameId: gameCollection.gameList[i]['gameObject']['id']
        });

    }

if (alreadyInGame == false){

console.log("Add them into a Game!!!")

        }
    });
});

Pop-Quiz time! Do you see a fallacy in the logic so far with the joinGame mechanism? (If you're good you may see several glaring issues).

Let's ask together out loud: "How do we deal with a User who clicks joinGame when no games have been made?"

Your guess is as good as mine, but I think, I would like to attack this by having the code create a new game object if there is not one available to join. To help facilitate this for later, let's be pre-emptive and separate out the building mechanism from the socket. To do this, on line 26 of index.js add in:

function buildGame(socket) {};

Okay cut and paste (move) the code that that is in the IF 'noGamesFound == true' statement. Put that code in between the squiggly brackets {} of our newly created function above. Doing this allows us to call the buildGame function anywhere in our code, so we don't have to duplicate it in other places. Go ahead and add in buildGame(socket) back into where the original code was moved from (should be on or near line 128). As things are configured currently, you should see an output like this in the console window:

multi-user app

To confirm, the buildGame function should like:

function buildGame(socket) {


     var gameObject = {};
     gameObject.id = (Math.random()+1).toString(36).slice(2, 18);
     gameObject.playerOne = socket.username;
     gameObject.playerTwo = null;
     gameCollection.totalGameCount ++;
     gameCollection.gameList.push({gameObject});

     console.log("Game Created by "+ socket.username + " w/ " + gameObject.id);
 
   io.emit('gameCreated', {
      username: socket.username,
      gameId: gameObject.id
    });


};

Announcing the game to both players

Let's actually build the means to append the user to that specific game object held in our gameCollection array. Our goal here is to replace "null" with the name of player 2 and to announce the game to both players.

To accomplish this, let's first create a new function, preferably underneath the buildGame function we made previously. The first thing we want to do in this object is to count the number of existing game objects. If that count is equal to zero, then we should use the buildGame function to create a new game on the spot (from my point of view, if a user had clicked to join a game, but no games existed, then naturally they should have a game created on their behalf that they now join in, as opposed to an error message telling them to click ok, then click 'create game' instead — why force an inferior user experience if we don't have to?).

Attach an ELSE statement to the end of that conditional IF that creates a game if none exist. Within that same ELSE statement, we will start with a random number generator. This generator must have the ability to count a number between 0 and the number of total games that exist. Our objective for that random number is to plug it into gameCollection.gameList[X], where X is that random value. By choosing a random value, we select a random game to query (at some point in the future should you choose to expand, you can opt for a different method to match users. For instance, if we were saving user info to a database, some attribute such as skill rating could be used to determine and pair players together).

Here is the line I used to generate a random variable:

var rndPick = Math.floor(Math.random() * gameCollection.totalGameCount);

Math.floor is required to round our value down to a whole number. Math.random() will only give us a floating point number between 0 and 1 (think .3784727927942) by multiplying it across the total game count, we can reliably pick a number between 0 and X, where X is the total number of game objects.

Now that we have picked a random game to query, we can use a simple IF statement to check if that specific game has a playerTwo value of NULL, and if so, set the playerTwo name for that specific game as the socket.username, like so:

if (gameCollection.gameList[rndPick]['gameObject']['playerTwo'] == null) {
    gameCollection.gameList[rndPick]['gameObject']['playerTwo'] = socket.username;
    socket.emit('joinSuccess', {
        gameId: gameCollection.gameList[rndPick]['gameObject']['id'] });

console.log( socket.username + " has been added to: " + gameCollection.gameList[rndPick]['gameObject']['id']);

} 

Once the value has been set, proceed to use socket.emit('joinSuccess') along with the related callback object to broadcast the joined gameId to the user.

What happens if that IF statement we defined above returns a game that is full, you ask? Attach an else statement to recursively call the gameSeeker function against itself — so it loops until a game is found; the circle of code iteration occurs and naturally we create a new bug.

Ask, "What happens when I try to join a room if all rooms are all full?" Now add in recursion. Any users beyond two that are odd numbered out will generate this error as a result:

multi-user app

One quick and dirty workaround was to create a variable that acts as a counter:

var loopLimit = 0; 

Increment this variable at each pass in the top of the joinGame function so that each time we use the function, it adds to the loopLimiter. In our initial check to see if the count of game objects is 0, we can use || to add an OR condition to ensure that we don't exceed the loopLimiter so that it will still generate a new game after X attempts to find a game. I've decided to pick a small number of 20 times to check a game at random. If more than 20 attempts occur, the code will create a new game for the user who clicked "Join Game."

(Note: A more elegant solution would be to find a room where player two equals null. It is statistically possible (however unlikely it may be) to hit the same games multiple times in that pass and effectively miss potential open games. My reasoning against building out that solution was that I'm lazy for one, and two, I feel that making an arbitrary small count would be more efficient on server resources — imagine having 20 users concurrently login to this app where there are an exactly even number of 4000 users across 2000 different games. If all 20 of them clicked join and the code is iterating through a FOR loop of over 2000 game objects, our server is going to have to compute availability 40,000 times instead of 400!)

Your code for the gameSeeker at this point should look similar if not identical to this:

function gameSeeker (socket) {
    ++loopLimit;
    if (( gameCollection.totalGameCount == 0) || (loopLimit >= 20)) {
        buildGame(socket);
        loopLimit = 0;

    }
     else {
        var rndPick = Math.floor(Math.random() * gameCollection.totalGameCount);
        if (gameCollection.gameList[rndPick]['gameObject']['playerTwo'] == null){
            gameCollection.gameList[rndPick]['gameObject']['playerTwo'] = socket.username;
            socket.emit('joinSuccess', {
                gameId: gameCollection.gameList[rndPick]['gameObject']['id'] });

            console.log( socket.username + " has been added to: " + gameCollection.gameList[rndPick]['gameObject']['id']);
    
        } 
        else {
            gameSeeker(socket);
        }
        }
}

Likewise, the complementing socket.on('joinGame') that requires this gameSeeker function should appear such that:

socket.on('joinGame', function (){

console.log(socket.username + " wants to join a game");

    var alreadyInGame = false;

    for(var i = 0; i < gameCollection.totalGameCount; i++){
        var plyr1Tmp = gameCollection.gameList[i]['gameObject']['playerOne'];
        var plyr2Tmp = gameCollection.gameList[i]['gameObject']['playerTwo'];
        if (plyr1Tmp == socket.username || plyr2Tmp == socket.username){
        alreadyInGame = true;
        
        console.log(socket.username + " already has a Game!");

        socket.emit('alreadyJoined', {
        gameId: gameCollection.gameList[i]['gameObject']['id']
        });

    }
}
    if (alreadyInGame == false){
        gameSeeker(socket);

    }
});

A quick troubleshooting

Midway through fleshing together and troubleshooting the join functionality, I found that the code still needed to parse for player two during the create game portion. In other words, my code for both buttons is now identical. Based on the solution determined above for the Join Game button, I've made the decision to remove the Create Game button as it is now redundant.

As it stands we can create a game OR find a game when using the "Join Game" button. Therefore, you will want to delete the socket.on('createGame') just above the complementing socket.on('joinGame') located at the bottom of index.js. Open main.js and remove the $createGame click-handler on line 236, the "var" initializer itself is on line 17, and the sendGame function is on line 287. To finish the cleanup, open the index.html file and remove the Create Game button on line 16. Add the following new button instead:

<button class='leaveGame' placeholder="Leave Game"/>Exit Game</button>
<br>

Let's also change line 19 of style.css to replace .createGame with .leaveGame. Let's make the button work. Copy $joinGame on line 18 off main.js and change those both to leaveGame on the .className and on the $variable name. Midway down this same file, copy the $joinGame click handler and replace with leaveGame as you did prior. The front-end should now be done (I hope), let's wrap up the server!

Finalizing the server

For the demo, I am going to set up an exit strategy like this: if playerOne is exiting, then destroy their specific game object, if playerTwo, on the other hand, is leaving, then we vacate their seat and allow a different user to join back in.

(In a real world situation, I advise that the game object is destroyed when either participant leaves and that the remaining player is awarded the "Win" condition of the game rules, to have their win/loss record updated accordingly.)

Since connecting this application to a persistent document store (such as a database) is outside of the scope of this tutorial, we can settle for allowing a conditional destroy. That way, you can see how to keep gameObjects open if one person leaves while also seeing how to destroy the very same object if the object creator, player one, leaves.

Take a few moments to step through the possible error-handling conditions we need to build for an exit button. Figured it out? If you said "We need to account for if a user is not currently in a game", you're spot on!

Jump back in main.js and add create two new socket.on events: notInGame and gameDestroyed. On or after line 326 in your main.js file, just under the leaveGame socket.on handler:

socket.on('notInGame', function () {
  log('You are not currently in a Game.');
});

socket.on('gameDestroyed', function (data) { 
  log( data.gameOwner+ ' destroyed game: ' + data.gameId);

});

Tying the functions to the back-end

Let's tie those functions to the back-end. By now you should realize that you need to go back to the index.js (server-side handling) file to create the socket.on(leaveGame) handler. At the very top of the callback function for this new handler, go on and add this IF statement:

if (gameCollection.totalGameCount == 0){
    socket.emit('notInGame');
}

If we don't add in this IF statement, our server throws an "Undefined" TypeError whenever there are zero game objects in the gameCollection; expect this edge case to occur when no game objects exist and a user attempts to "Leave Game." No game objects exist, therefore, any gameObjects you attempt to call are "Undefined," hence the error.

Following this IF, add an ELSE block. Within that ELSE block, we're going to (just as in JoinGame) use a FOR loop to iterate across all gameObjects in the collection to check if a user is connected to a game and either Destroy the game object (for player 1) or remove the player from the game object (for player 2). To increase code readability, let's also take a moment to locally define the game object's ID along with the names of player one and two. So far what we have built should resemble this:

  socket.on('leaveGame', function() {
    if (gameCollection.totalGameCount == 0){
       socket.emit('notInGame');
    }

    else {
        for(var i = 0; i < gameCollection.totalGameCount; i++){
             var gameId = gameCollection.gameList[i]['gameObject']['id']
             var plyr1Tmp = gameCollection.gameList[i]['gameObject']['playerOne'];
             var plyr2Tmp = gameCollection.gameList[i]['gameObject']['playerTwo'];
   …

Similar right!?!

Check if username matches the right player

Okay, now that we have defined our game ID and players, we can run IF statements to check if our socket.username matches player 1 or 2. If the socket.username is indeed player one, first decrease the total number of game objects so that the count matches the number of game objects held in the gameCollection, like so:

--gameCollection.totalGameCount;

To actually delete the object itself from our gameCollection array, we rely on our trusty locally scoped i variable that was defined at the beginning of our FOR loop. JavaScript's SPLICE statement will remove an element from an array. The syntax used is essentially: myArray.splice(elementIndexNumber, #ofElementsToRemove). That i variable is the index. We're only removing one variable which means our example looks like this:

gameCollection.gameList.splice(i, 1);

With the game object gone, as a sanity check, we should console.log() out the the entire gameCollection.gameList array:

console.log(gameCollection.gameList);

Emitters

Finally, let's put in our emitters to announce back outwards onto the front-end. First, a stock one for player one to see that he has left the game, then a second one to announce to everyone that the game with an ID of "X" has been destroyed (to inform player 2 that his game has been destroyed). The IO emitter should include the socket.username as well as the gameId in its outbound object elements so that the data is broadcasted to all users. As a reference, the complete playerOne IF block should now look like this:

if (plyr1Tmp == socket.username){
    --gameCollection.totalGameCount; 
    
    console.log("Destroy the Game!");
    
    gameCollection.gameList.splice(i, 1);

    console.log(gameCollection.gameList);

    socket.emit('leftGame', { gameId: gameId });
    io.emit('gameDestroyed', {gameId: gameId, gameOwner: socket.username });
} 

Player two logic handling is a little easier. Add an ELSE IF statement that checks if socket.username is equal to plyr2Tmp. Don't be tempted to:

else if (plyr2Tmp == socket.username) {
   plyr2Tmp = null;
…

...because you are changing the temporary variable, but not the actual object properties:

multi-user app

After you set gameCollection.gameList[i]['gameObject']['playerTwo'] to equal null, go on and console.log() the events so they appear in the server side console screen for reference and, as before, socket.emit the leftGame event to our player two user.

For good measure, console.log() this specific game object to confirm that the user is taken off the game list. Remember we're working with an ELSE IF, which means there is a final ELSE condition to handle when a user clicks "Exit Game" but has no game…again, emit notInGame. In all, our socket.on('leaveGame') object should resemble:

socket.on('leaveGame', function() {
    if (gameCollection.totalGameCount == 0){
       socket.emit('notInGame');
    }

    else {
        for(var i = 0; i < gameCollection.totalGameCount; i++){
            var gameId = gameCollection.gameList[i]['gameObject']['id']
            var plyr1Tmp = gameCollection.gameList[i]['gameObject']['playerOne'];
            var plyr2Tmp = gameCollection.gameList[i]['gameObject']['playerTwo'];
          
              if (plyr1Tmp == socket.username){
                    --gameCollection.totalGameCount; 
                   console.log("Destroy the Game!");
            gameCollection.gameList.splice(i, 1);
            console.log(gameCollection.gameList);
            socket.emit('leftGame', { gameId: gameId });
                    io.emit('gameDestroyed', {gameId: gameId, gameOwner: socket.username });
               } 
               else if (plyr2Tmp == socket.username) {
                    gameCollection.gameList[i]['gameObject']['playerTwo'] = null;
                console.log(socket.username + " has left " + gameId)
            socket.emit('leftGame', { gameId: gameId });
            console.log(gameCollection.gameList[i]['gameObject'])
        } 

              else {
                    socket.emit('notInGame');
             }
         }
    }
});

One more question for you: As our code is currently configured, what happens when we have four users connect to the server, make games, join them, a fifth player enters, and immediately presses "Exit Game" first?

Due to our current logic holding the final ELSE notInGame socket emitter WITHIN the FOR loop, we're seeing the not in game message emitted at each pass of the loop (looks messy). To account for our FOR loop iteration, we should define a locally scoped variable (just as we did in alreadyInGame) and set its default value to FALSE: 'var notInGame = false;'.

Remove the notInGame socket emitter from the ELSE block within the FOR loop and replace with: notInGame = true;.

Again for reference:

 socket.on('leaveGame', function() {

    var notInGame = false;
    if (gameCollection.totalGameCount == 0){
       socket.emit('notInGame');
     
    }

    else {
        for(var i = 0; i < gameCollection.totalGameCount; i++){

          var gameId = gameCollection.gameList[i]['gameObject']['id']
          var plyr1Tmp = gameCollection.gameList[i]['gameObject']['playerOne'];
          var plyr2Tmp = gameCollection.gameList[i]['gameObject']['playerTwo'];
          
          if (plyr1Tmp == socket.username){
            --gameCollection.totalGameCount; 
            console.log("Destroy the Game!");
            gameCollection.gameList.splice(i, 1);
            console.log(gameCollection.gameList);
            socket.emit('leftGame', { gameId: gameId });
            io.emit('gameDestroyed', {gameId: gameId, gameOwner: socket.username });
           } 
          else if (plyr2Tmp == socket.username) {
            gameCollection.gameList[i]['gameObject']['playerTwo'] = null;
            console.log(socket.username + " has left " + gameId)
            socket.emit('leftGame', { gameId: gameId });
            console.log(gameCollection.gameList[i]['gameObject'])

           } 

          else {
            
            notInGame = true;
         }


      }

      if (notInGame == true){
        socket.emit('notInGame');
      }
    }

  });

Even though we changed the code to only emit notInGame once per user, we have the unintended consequence of triggering notInGame once a gameObject connected to player 1 is destroyed because we are STILL iterating through the FOR loop, which triggers the ELSE notInGame = true; still. Though this glitch works to our advantage at the moment, I would prefer that we address this unintended behavior so that our code performs exactly how we intend and not this:

multi-user app
The username in this particular window is BigL

I tried to use BREAK statements within the IF and IF ELSE blocks in the FOR loop. However, this approach did not work due to the fact that we don't know which index matches a given player. It could be the fourth game in the list or even the second, who knows?

With how our code functions now, that means, we trigger notInGame to equal TRUE before our FOR loop reaches the matching player's gameObject, causing the "You are not currently in a game" warning to occur when it shouldn't. It turns out that we can simply flip the assignment of that variable notInGame to equal true by default, then assign a false value whenever a matching value is found in the loop. Since we already moved the IF statement that checks the notInGame outside of the FOR loop, it functions only once after the FOR loop finishes iterating across all game objects! Thus, our code snippet now looks like this:

 var notInGame = true;
  for(var i = 0; i < gameCollection.totalGameCount; i++){

    var gameId = gameCollection.gameList[i]['gameObject']['id']
    var plyr1Tmp = gameCollection.gameList[i]['gameObject']['playerOne'];
    var plyr2Tmp = gameCollection.gameList[i]['gameObject']['playerTwo'];
    
    if (plyr1Tmp == socket.username){
      --gameCollection.totalGameCount; 
      console.log("Destroy Game "+ gameId + "!");
      gameCollection.gameList.splice(i, 1);
      console.log(gameCollection.gameList);
      socket.emit('leftGame', { gameId: gameId });
      io.emit('gameDestroyed', {gameId: gameId, gameOwner: socket.username });
      notInGame = false;
     } 
    else if (plyr2Tmp == socket.username) {
      gameCollection.gameList[i]['gameObject']['playerTwo'] = null;
      console.log(socket.username + " has left " + gameId);
      socket.emit('leftGame', { gameId: gameId });
      console.log(gameCollection.gameList[i]['gameObject']);
      notInGame = false;

     } 

  }

  if (notInGame == true){
  socket.emit('notInGame');
  }

Polishing the server

We're just about at the home stretch. To give our MatchMaking server that final polished feel, let's modify our design slightly such that when a player leaves our gameServer (closes the browser or refreshes the page), their game Object is automatically destroyed (if player one) or their name is removed (if player two). To accomplish, simply move the ENTIRE logic of the outermost ELSE statement within socket.on('leaveGame') into its own standalone function. I placed mine near the top of index.js below the function buildGame (on or near line 46). Here is what the new standalone function looks like:

function killGame(socket) {

  var notInGame = true;
  for(var i = 0; i < gameCollection.totalGameCount; i++){

    var gameId = gameCollection.gameList[i]['gameObject']['id']
    var plyr1Tmp = gameCollection.gameList[i]['gameObject']['playerOne'];
    var plyr2Tmp = gameCollection.gameList[i]['gameObject']['playerTwo'];
    
    if (plyr1Tmp == socket.username){
      --gameCollection.totalGameCount; 
      console.log("Destroy Game "+ gameId + "!");
      gameCollection.gameList.splice(i, 1);
      console.log(gameCollection.gameList);
      socket.emit('leftGame', { gameId: gameId });
      io.emit('gameDestroyed', {gameId: gameId, gameOwner: socket.username });
      notInGame = false;
     } 
    else if (plyr2Tmp == socket.username) {
      gameCollection.gameList[i]['gameObject']['playerTwo'] = null;
      console.log(socket.username + " has left " + gameId);
      socket.emit('leftGame', { gameId: gameId });
      console.log(gameCollection.gameList[i]['gameObject']);
      notInGame = false;

     } 

  }

  if (notInGame == true){
  socket.emit('notInGame');
  }


}


In tandem with moving the core logic we now have a cleaner looking socket.on('leaveGame'):

  socket.on('leaveGame', function() {


    if (gameCollection.totalGameCount == 0){
       socket.emit('notInGame');
     
    }

    else {
      killGame(socket);
    }

  });

Wrapping up

And there you have it! We've extended a stock lightweight socket.io chat application into a rudimentary matchmaking server. This, in and of itself, is far from a minimum viable product (MVP), though you can use the foundation laid here to build a multi-user application in Node.js. Some additional features and functionality I would want to be added in include but are not limited to:

  1. Matchmaking based on skill ranking, which requires:
    • data persistence (connecting and saving users and user data to a structure to retrieve userIDs, win/loss counts, highest score achieved, and skill-rating attributes saved and retrieved).
  2. The ability to force unique names per users (as it stands, the current code allows for duplicate usernames. One approach is perhaps to have a common display name, but a hidden internal Unique Identifier for users so there is never an issue of duplicate user IDs. As the logic is built now, we do gameObject updates based on the usernames, which is a problem if two users named "Joe" come to play — the system will destroy both game objects and/or prevent them from both being able to play their own unique games.
  3. Perhaps being able to select a particular game to join by way of buttons.
  4. A list of open game URLs — where clicking that URL essentially joins that specific game.
  5. Building an actual room endpoint such that users are moved into a separate private game page — where the actual game code itself is held.

With those features fleshed out, I would then pursue the actual buildout of the game itself which would include the game engine, scoring logic, controls setup for both players, and any anti-cheating countermeasures (perhaps user input velocity detection, or forcing the client to send an updated snapshot of current game condition variables — set up a heuristics filter that examines the past X number of game-frames. This way, if a user is cheating and spoofing their game condition variables or feeding illegal moves back to the server, I would have the server send a forfeit signal to force the cheater to quit the game and lose (and maybe, if I was feeling particularly evil, would set up a specialized label denoting such a user as "cheater").

If you have found a mistake or a bug in the GitHub repo of this project file or perhaps if you think this implementation is too amateur for your taste, then, by all means, speak up. I'd prefer that you teach me a thing or two occasionally too! Conversely, if you found this too arduous to traverse and/or need additional clarification, utilize the electronic carrier pigeon commenting system below to summon me! Thank you for your time with us today, until next time.


Author's Bio

multi-user app
Frankenmint is a technophile with a penchant for Bitcoin, versed in web and software development.

Discover and read more posts from Codementor Team
get started
post commentsBe the first to share your opinion
rave
6 years ago

Beautiful code! only sad that chat from different rooms can be seen in all rooms, besides that wonderful! XOXO =)

Mohamed
7 years ago

Can you please make part 3

Notch M
7 years ago

Another question, why not use socket.io rooms instead of your method?

Show more replies