Connect 4 With Socket.io

The game of Connect 4 brings back yesteryear memories. This classic game has surely left an impression on everyone who has played it. In this article, we're going to make a multiplayer version of Connect 4 using Node.js and Socket.io.

Installing the Dependencies

This tutorial assumes that you have Node.js and npm installed. To manage the frontend dependencies, we'll use Bower to fetch the packages and Grunt to manage tasks. Open a terminal and install Bower and Grunt globally by executing:

Note: Grunt requires Node.js versions >= 0.8.0. At the time of writing this article, Ubuntu's repositories had an older version of Node. Please make sure you're using Chris Lea's PPA if you're on Ubuntu. For other distributions/operating systems, please refer to the Node.js installation docs for getting the latest version.

With Bower and Grunt-cli installed, let's create a directory for the project and fetch the Twitter Bootstrap and Alertify.js (to manage alert notifications) using Bower.

Now, let's set up a directory to manage our custom assets. We'll name it assets and store our custom Less and JavaScript files inside it.

To serve the compiled assets, we'll create a directory named static with sub-directories named javascript and stylesheets.

Open assets/stylesheets/styles.less and import variables.less and the required Less files from bootstrap.

With that done, let's setup the Gruntfile.js to compile the Less files in to CSS and combine all the JavaScript files into one single file. The basic structure of the Gruntfile.js file with some tasks, would look something like this:

Asset Tasks

We'll define three tasks to manage the assets. The first one will be to compile all the Less files to CSS. The second one will be to concatenate all JavaScript files into one and finally, the last task will be to watch files for changes. The watch task would be the default task and can be run by typing grunt in the project root, once we're done configuring the gruntfile.

Let's setup a task to compile all Less files to CSS files in the static/stylesheets directory.

Moving on, we'll setup another task to concat all the JS files into one.

Finally, let's set the watch task to watch our files for changes and execute the required tasks on save.

With that done, we'll load the required npm plugins and register the default task.

Let's move on to managing backend dependencies using npm. For this project, we'll be using the Express framework with the Jade templating engine and Socket.io. Install the dependencies locally by executing the following command:

The directory structure should now be similar to this:

2014-02-05-132602_239x498_scrot

Now that we've got our dependencies set up, it's time to move on to making the frontend of our game.

The Frontend

Let's continue by creating a file named server.js and serve content using Express.

The Templates

We're using the Jade Templating Engine to manage the templates. By default, Express looks for views inside the views directory. Let's make the views directory and create Jade files for the layout, index and the thanks page.

Next, let's edit the layout of our project, index page and landing page(landing.jade).

Notice that we're serving socket.io.js, although it's not defined anywhere in the static directory. This is because the socket.io module automatically manages the serving of thesocket.io.js client file.

The Styling

Now that we've got the HTML setup, let's move on to defining the styles. We'll begin with overwriting some bootstrap variables with the values of our choice inside assets/stylesheets/variables.less.

Then we'll append some custom styles to styles.less.

The JavaScript

With that done, let's add some JavaScript code in assets/javascript/frontend.js to create the grid and add data-row and data-column attributes with proper values dynamically.

That covers the frontend setup. Let's compile the assets and launch the server.

If you've been following along, the index page should look similar to this:

2014-02-05-133240_1363x616_scrot

Tip: Run the grunt command on the project root on a separate terminal. That would call the default task which happens to be watch. This would concat all JS files or compile all Less files on every save.

The Backend

The objective in Connect 4 is to connect four consecutive "blocks" either horizontally, vertically or diagonally. Socket.io allows us to create rooms that clients can join. Think of them as IRC channels. 

The Game Rooms

We'll use this feature in the game, so that only two players may be in a room and the room is destroyed when either of them quits. We'll create an object that will keep track of all rooms and in turn, all game-states. Let's begin with creating a function in server.js to create random room names.

The function expects the length of the random room name we wish to generate. The room name is generated by concatenating random characters from the haystack string. Let's modify our route for the index page to include the share URL and create a new route to serve the content if a particular room is accessed.

In the above code, we generate the share ID using the generateRoom() function we defined previously and pass in the share ID and URL as parameters to the template. The second route expects a parameter named room which is restricted by a regular expression. The regex allows a string containing only alphanumeric characters, of length six. Again, we pass on the shareURL and id as parameters to the template. Let's add some attributes to the input element of our index so that we may access them in frontend.js later.

Next, let's edit frontend.js to connect to the Socket.io server, join the room and assign some properties to the current player.

Note that we created an object called player to refer to the player on the client side. On connection, the join event is called on the backend which inturn emits the assign emit on the frontend to assign some properties to the player. We may now proceed to define the code in the backend to handle the join event.

Note: The Socket.io event handlers on the backend should be added inside the io.sockets.on('connection', function(socket) { } code block. Similarly, the event handlers and frontend JavaScript code should be inside the $(document).ready(function() { } code block.

In the code above, we defined the event handler for the join event that is emitted by the frontend. It checks whether the given room already exists and whether player two hasn't already been assigned and if so, assigns the current client as player two. Otherwise, it assigns the current client as player one and initializes the board. We emit the leave event on the frontend for clients who attempt to join a game in progress. We also set some properties on the socket using socket.set(). These include room id, color, pid and the turn variable. The properties set this way may be retrieved from the callback of socket.get(). Next, let's add the leave event handler on the frontend.

The leave event handler simply redirects the client to the landing page. We now proceed to emit an event that alerts both the players if the game is ready to begin. Let's append some code to the if condition of our join event on the server side.

We must define a notify event in the frontend that deals with the notification. Alert.js provides a neat way to handle all the notifications. Let's add the notify event handler in frontend.js.

Time to try out our progress so far. Launch the server locally and access localhost and the share URL in two separate windows. If you've been following along, you should be greeted with an alert on the bottom right corner, as shown in the image below:

2014-02-05-232920_1362x615_scrot

Adding Interactivity

Now let's proceed to add code that emits an event when the blocks are clicked. For this part, we need to ascertain whether the click was made by the correct player. This is where the turn property we set on the socket would come into play. Append the following code to frontend.js.

The above code sets an event handler on all the table cells. One thing to note is that the grid in Connect 4 is similar to adding bricks in a wall, that is, one may not fill a particular (row, column) pair if (row-1, column) pair isn't filled. Therefore, we must first get the (row, column) pair of the cell that was clicked and then work out a way to determine the actual cell to be filled. This is done in the backend, in the event handler for click.

The above event handler uses the async module to fetch the socket properties simultaneously. This avoids nesting callbacks in successive uses of socket.get(). The results variable is an array with elements in the same order as the socket.get() calls. results[0], therefore refers to turn and so on. 

Once the properties have been fetched, we swap the turns and figure out the (row, column) pair to fill. We do this in the while loop by starting from the bottom row (row five) and moving our way upwards until the value of the board at (row, column) is zero (which implies it has not been played on). We then assign the pid (either one or negative one) to the element on the board and emit the drop event on both players. Let's add the drop event handler on frontend.js and introduce an animation that gives us a falling effect.

We implement the drop animation using JavaScript's setInterval() method. Starting from the topmost row (row zero), we continue calling fillBox() in intervals of 25 seconds until the value of row equals the value of data.row. The fillBox function clears the background of the previous element, in the same column and assigns a background to the current element. Next, we come to the crux of the game, implementing the winning and draw conditions. We'll cover this in the backend.

We begin by defining a helper function that returns four (row, column) pairs either horizontally, vertically or diagonally. The function expects the current row and column and an array that determines the increment in row and column values. For instance, a call to getPair(1,1, [1,1]) would return [[1,1], [2,2], [3,3], [4,4]] which happens to be the right diagonal. This way we can get respective pairs by selecting suitable values for the step array. We've also declared a list to hold all the functions that check for wins. Let's begin by going through the function that checks for wins horizontally and vertically.

Let's go through the above function step by step. The function expects four parameters for room, row, column and the success callback. To check for a win horizontally, the cell that was clicked on might contribute to a winning condition in a maximum of four ways. For instance, the cell at (5, 3) may result in a win in any of the following four combinations: [[5,3], [5,4], [5,5], [5,6]], [[5,2], [5,3], [5,4], [5,5]], [[5,1], [5,2], [5,3], [5,4]], [[5,0], [5,1], [5,2], [5,3], [5,4]]. The number of combinations might be less for border conditions. The algorithm above deals with the problem in hand by calculating the left most column (variable column) and right most column (variable columnEnd) in each of the four possible combinations.

If the right most column is greater than six, it's off the grid and that pass can be skipped. The same will be done if the left most column is less than zero. However, if the edge cases fall in the grid, we compute the (row, column) pairs using the getPair() helper function we defined previously and then proceed to add the values of the elements on the board. Recall that we assigned a value plus one on the board for player one and negative one for player two. Therefore, four consecutive cells by one player should result in a count of either four or negative four respectively. The callback is called in case of a win and is passed two parameters, one for the player (either one or two) and the other for the winning pairs. The function that deals with the vertical check is quite similar to the horizontal one, except that it checks the edge cases in rows rather than columns.

Left and Right Diagonals

Let's move on to defining checks for the left and right diagonals.

The checks for diagonals are quite similar to the ones for horizontal and vertical checking. The only difference is that in the case of diagonals, we check edge cases for both rows and columns. Finally, we define a function to check for draws.

Checking for draws is rather trivial. A draw is obvious if all the cells in the top row have been filled and nobody has won. Thus, we rule out a draw if any of the cells in the top row has not been played on and call the callback otherwise.

With the winning and draw conditions sorted out, we must now use these functions in the click event and emit a reset event on the frontend for denoting the clients the end of the game. Let's edit the click event to handle these conditions.

In the code above, we check for a win horizontally, vertically and diagonally. In case of a win, we emit the reset event on the frontend with an appropriate message for both players. The highlight property contains the winning pairs and the inc property denotes the increment score for both of the players. For instance, [1,0] would denote increasing player one's score by one and player two's score by zero. 

Let's proceed by handling the reset event on the frontend.

In the reset event handler, we highlight the winning pairs after 500ms. The reason for the time delay is to allow the drop animation to finish. Next, we reset the board in the background and popup an alertify confirmation dialog with the text sent from the backend. In case the user decides to continue, we emit the continue event on the server side or else redirect the client to the landing page. We then proceed to increase the player scores by incrementing the current score by the values received from the server.

Next, let's define the continue event handler in the backend.

The continue event handler is quite straightforward. We emit the notify event again and the game resumes in the frontend.

Next, let's decide on what happens when either of the players gets disconnected. In this scenario, we should redirect the other player to the landing page and remove the room from the gamestate. Let's add this feature in the backend.

The above event handler would broadcast the leave event to the other players and remove the room from the game's object, if it still exists.

Conclusion

We've covered quite a lot of ground in this tutorial, starting with getting the dependencies, creating some tasks, building the front and back-end and ending with a finished game. With that said, I guess it's time for you guys to have some rounds with your friends! I'd be glad to answer your questions in the comments. Feel free to fork my repo on GitHub and improvise on the code. That's all folks!

Tags:

Comments

Related Articles