Socket.IO Checkers game with Redis and Angularjs

For awhile now I've been thinking about writing a multi player game to make use of socket.io and to throw something else in the mix I thought that I'd give Redis a go for the cacheing layer.

Now, I wasn't aiming to be too ambitious but wanted something that would be fun to play and so I opted for checkers.

I firstly started trying out the chat demo that is on the socket.io site to get to get grips with things and to ensure that I am actually doing something right.

BUT then it was time to spice things up a bit.

My folder structure is as follows:

root
-- bower_components (for client side modules)
-- node_modules (for server side modules)
-- public folder
    checkers.js
    index.html
    style.css
    fonts folder
redis.js
app.js

I'll break this down into Server side & Client side and hopefully things will join up at the end.

Server side

I'm a fan of express and its something that I comfortable with so we'll start by building out the bones of an express server by first installing express with npm install express http --save

NB thats --save and not --save-dev a mistake that I learned in a previous post.

var express = require('express');
var app = express();
var http = require('http');
var uuid = require('node-uuid'); //used for creating games. 
...
http.listen(3000, function(){
    console.log('http server listening on port 3000');
});

and now to get to interesting part. I'll be using the socket.io module from the chat demo and installing it with npm install socket.io --save and then creating a socket variable which will integrate with the http server.

var socketio = require('socket.io')(http);

and then use the socketio variable to listen for events and pickup the sessionid from the client.

socketio.on('connection', function(socket){
    console.log('user connected', socket.id);
    socket.on('disconnect', function(){
        console.log('user disconnected');
    }         
]);

I'll also create a dispatcher that I can pass around when recreated to send messages out which will take 3 arguments:

  • event
  • message to send
  • the session id

and the dispatcher...

var dispatcher = function(type, obj, toid){
        console.log(obj);
        socketio.to(toid).emit(type, obj);
};

Now that we have the bare server and socket listener we can get our redis server installed on my linux box with apt-get install redis-server. See this guide

NB To start the redis server locally use redis-server, and to connect to the redis server in the terminal use redis-cli.

To enable our express server to talk to the redis server we'll install the redis module with npm install redis --save and we'll create a new _redis.js file to split off our redis logic.

var redis = require('redis');
var client = redis.createClient();
client.on('connect',function(){
    console.log('connected to redis');
});

var exports = module.exports;

As we are splitting out the different logic into their own specific js files this will have to be brought aback into our app.js file using var redis = require(./_redis.js); in the app.js file.

Now we have a redis connection we will create a method to create a new game when a user connects.

In the redis.js file we create method passing in the sessionid and dispatcher to send messages to the client which will also be exposed to the app.js file using exports.

exports.createGame = function(sessionid, dispatcher){
    var gamename = 'game';
    client.hmset(
        gamename: {
            player1: sessionid,
            player2: ''
        }
    );
    dispatcher('game', {name: gamename, player: 'player1'}, sessionid);
});

When writing the data to the redis server we use hmset.

Redis uses key value pairs to keep the data and here we are setting the key as 'game' and using a hash to record the object. Lastly we are calling the dispatcher which sends a 'game' event with the gamename along with telling the player that they are player1 (this is useful later on) to the connected socket id.

So, the concept of the checkers game is that first user that connects will create the game and when another connection is made then they are player 2.

For this when a new connection is made we will have to check if there are any games that do not have a player2 assigned to it. We'll create a new function in redis.js to handle this: redis.checkAwaitingGames(socket, associateGame, dispatcher);

exports.checkAwaitingGames = function(socket, callback, dispatcher){
    ...
}

To check that there isn't a game waiting for a second player we'll use the client object again to loop through the current keys.

var foundGame = false; // we'll use this to exit if a game is found
client.keys('*', function(err, games){
    // '*' is a wild card to return all the keys.

    // games is an array so check its length.
    if(games.length > 0){

    }
    else {
        callback({game: '', found: false, socket: socket});
    }
}

If we don't find any open games then call the associateGame to create or (if we do) assign the game.

So, if we do have a list of games then we can loop over the list and as redis stores key value pairs we use hgetall along with the key game to return the value and test to see if there is a player2 sessionid. If we find one then tell associateGame we have a game and also send a message to player 1.

games.forEach(function(game, i){
    if(!foundGame){
        client.hgetall(game, function(err, reply){
            if(reply.player2 === '' && reply.player1 !== socket.id){
                callback({game: game, found: true, socket: socket});
                foundGame = true;
                dispatcher('player2 found', {player: socket.id}, reply.player1);
            }
        });
    }
});

We have spoke about associateGame without defining it. It is a handler defined in app.js to create or assign a game which is done by checker_redis.js.

var associateGame = function(obj){
    if(obj.found){
        //console.log('associateGame assignGame');
        redis.assignGame(obj.game, obj.socket.id, dispatcher);
    }
    else{
        //console.log('associateGame createGame');
        redis.createGame(obj.socket.id, dispatcher);
    }
};

As you can image assignGame & createGame will be writing stuff to redis using hgetall & hmset and then send a message to the connecting user with dispatcher.

exports.assignGame = function(game, socketid, dispatcher){
    client.hgetall(game, function(err, reply){
        if(err){
            exports.createGame(socketid, dispatcher);
        }
        else{
            reply.player2 = socketid;
            client.hmset(game, reply);

            var message = {
                name: game,
                player: 'player2'
            }
            dispatcher('game', message, socketid);
        }
    });
};

First part of assignGame will return the value for the key that was passed but if there is an error return then call createGame otherwise we'll update the value with the player2 sessionid and then update the key with hmset. Lastly sending a message to the connecting session with the game details along with telling the client that they are player2.

createGame is much simpler as we already know we haven't got anything to find but to just add a new game. I'm using uuid to ensure that I don't try to add the same key name and pass in a new hash along with the uuid. And thats it. Next is to create a message to the connecting user to send the game details and that the client is player1.

exports.createGame = function(sessionid, dispatcher){
    var uuid1 = uuid.v4();
    client.hmset(uuid1, {
        'player1': sessionid,
        'player2': ''
    });

    var message = {
        name: game,
        player: 'player1'
    };

    dispatcher('game',message, sessionid);
}

So far we've been able to connect to the server, search for a game, join it if there isn't a player2, or create a new game. Now we're going to handler when a move is actually made. For this we'll require to listen out for another type of socket message in our app.js file, "move taken".

socket.on('move taken', function(obj){
    console.log('move taken', obj);
    redis.getOpponent(obj, dispatcher);

});

So here a user has taken a move and the client has sent the move taken message along with an object specifying the move. The server will simply send this on to the opponent and does this in the redis.getOpponent handler and once again passing the dispatcher to be used when sending the message.

The message will contain the game key and whether the client is player1 or player2. We use exports so that the getOpponent is available when redis.js instantiated .

client.hgetall(obj.name, function(err, found){

    if(obj.player ==='player1'){
        dispatcher('move taken', obj, found.player2);

    }
    else{
        dispatcher('move taken', obj, found.player1);
    }
});

The client is called along with hgetall to find the key value pair and so the correct session will receive the message.

To tell the opposition that one of their pieces has been taken we'll use a similar handler called sendTakenPiece and socket will be listening for 'taken piece'.

client.hgetall(obj.name, function(err, found){
    if(obj.player ==='player1'){
        dispatcher('taken piece', obj, found.player2);
    }
    else{
        dispatcher('taken piece', obj, found.player1);
    }
});

Finally i want to keep redis in as light a state as possible and so on player1 disconnect i will delete the game. Socketio will be listening for 'disconnect' and will call redis.closeGame.

Ideally we would know which game we are deleting but on disconnect will not give us that information but will give us the sessionid of the disconnecting client. In closeGame we'll be looping over the games that we have and check the game.player1 is the sessionid of the client.

client.keys('*', function(err, games){
    games.forEach(function(game, i){

        client.hgetall(game, function(err, reply){

            if(reply.player1 === sessionid){
                client.del(game);
            }

            if(reply.player2 ===sessionid){
                dispatcher('player offline', {player: sessionid}, reply.player1);
            }

        });
    });
});

Just to keep a bit of control we'll only delete the game when player1 disconnects.

But that is it. You have a server that will handle messages from the checkers client server code.

Client side

The Client side is simply three files.

  • Checkers.js
  • index.html
  • style.css

This is what the html for the board layout will look like:

<table id="chess_board" cellpadding="0" cellspacing="0">

        <tr>

            <td id="E1">
                <i ng-if="checkPosition('A1')===false" ng-click = "movehere('A1')" ng-class="getPositionClass('A1')">&nbsp;</i>
                <i ng-if="checkPosition('A1')===true" ng-click = "pieceselected('A1')" ng-class="getPositionClass('A1')"></i>
            </td>
            <td id="E2">
                <i ng-if="checkPosition('A2')===false" ng-click = "movehere('A2')" ng-class="getPositionClass('A2')">&nbsp;</i>
                <i ng-if="checkPosition('A2')===true" ng-click = "pieceselected('A2')" ng-class="getPositionClass('A2')"></i>
            </td>
            <td id="F3">
                <i ng-if="checkPosition('A3')===false" ng-click = "movehere('A3')" ng-class="getPositionClass('A3')">&nbsp;</i>
                <i ng-if="checkPosition('A3')===true" ng-click = "pieceselected('A3')" ng-class="getPositionClass('A3')"></i>
            </td>
            <td id="E4">
                <i ng-if="checkPosition('A4')===false" ng-click = "movehere('A4')" ng-class="getPositionClass('A4')">&nbsp;</i>
                <i ng-if="checkPosition('A4')===true" ng-click = "pieceselected('A4')" ng-class="getPositionClass('A4')"></i>
            </td>
        </tr>

        ...

There will be 6 tr's and 4 td's. Just to keep things simple and as I've decided to a predetermined table size i've created an array to hold each square on the board called pieces and to predefine where the player pieces are already situated.

vm.pieces = 
        [
            {position: 'A1', class: vm.icons.player2, player: 'player2', queen: false},
            {position: 'A2', class: vm.icons.blank, player: undefined, queen: false},
            {position: 'A3', class: vm.icons.player2, player: 'player2', queen: false},
            {position: 'A4', class: vm.icons.blank, player: undefined, queen: false},...

Each piece is given (initially) a class, player, and queen value which can change as the game moves on. The class is the icon that will be displayed and a scope method is used to return the class - $scope.getPositionClass which uses lodash to query the array.

$scope.getPositionClass = function(position){
        return _.where(vm.pieces, {position: position})[0].class;
    };

The player is whether it is the client or the opponents piece at that grid reference, and the Queen is if the piece can move both directions of the board.

The ngClass is used to display the checker icons. You'll notice that i have adopted the controller as approach so I've used <body ng-app="checkers" ng-controller="checkerctrl as vm"> so that scope shadowing isn't an issue (because using ngIf creates a separate scope) and var vm = this;.

I use checkPosition to check if the position on the grid already has a checker piece on it.

$scope.checkPosition = function(position){
        if(_.where(vm.pieces, { position: position })[0].player === undefined){
            return false;
        }
        return true;
    };

To start the socket connection we use var socket = io();. Which opens the socket connection with the server and the first function we'll create is to handle creating a game on connetion.

    socket.on('game', function(game){
        vm.game = game;
        if(vm.game.player==='player1'){
            //vm.myturn = true;
        }
        $scope.$apply();
    });

Socket is looking for an event of game when the user first connects and sets the local variable of the game details to the game passed in the event. The $scope.$apply() is used to start the $digest cycle so the bindings are updated. player 2 found is also waiting for the server to tell it that a second player has joined the game so that it can begin. vm.myturn ensures that no moves can be made without another player being present or to stop cheating as it is the opponents move.

    socket.on('player2 found', function(game){
        vm.myturn = true;
        vm.player2Found = true;
        $scope.$apply();
    });

Next we'll handle if the opponent quites the game with:

    socket.on('player offline', function(msg){
        if(vm.game.player ==='player1'){
            vm.player2Found = false;
        }

        vm.myturn = false;
        $scope.$apply();
    });

As the game is player 1 driven it only makes a difference to the system if player 2 quits the game (because if you recall we delete the game from the redis db if player 1 quits). We set vm.myturn to false to ensure that no moves can be made.

Now to move onto something more interesting. With the help of the ngIf the board is split into grids that have pieces on the grid and those without. When a player clicks on a piece to move this piece and its grid reference are recorded in $scope.pieceselected.

$scope.pieceselected=function(position){    
        vm.validMoves = [];
        vm.selectedpiece = _.where(vm.pieces, { position: position })[0];
        vm.GetValidMoves();
    };

Here we are using lodash to search through the array to find the correct item in the array and then storing the item in vm.selectedpiece to be used later. As a side note i can not recommend lodash enough if you are using arrays to hold data and require to query the data.

Also as part of the piece selection vm.GetValidMoves() is called to build a list of valid moves that a piece could make. As both players start by moving in the opposite directions there must be a seperate way of building the list.

vm.GetValidMoves = function(){
    var index = rows.indexOf(vm.selectedpiece.position.split('')[0]);
    var colindex = parseInt(vm.selectedpiece.position.split('')[1]);
    if(vm.selectedpiece.player==='player2' || vm.selectedpiece.queen ===true){
        if(index < rows.indexOf('F')){
            var Row = rows[index + 1];
        ...
        }
    }
    if(vm.selectedpiece.player==='player1' || vm.selectedpiece.queen ===true){
        if(index > rows.indexOf('A')){
            var Row = rows[index - 1];
        ...
        }
    }
}

The row and the column is then passed to another method vm.checkDiagonal(Row + (colindex + 1)); to ensure that the position is not already occupied and added the position to the array vm.validMoves.

When a player makes a move angular will call $scope.movehere passing in the position in the checkers grid that the players piece is moving to.

$scope.movehere=function(position){
....
}

A lot of this section is made up of checking that the move id valid or that its the correct player that is moving. If it's not then return false.

    if(!vm.validateMove(vm.selectedpiece.position, position))
            {
                return false;
            }
            vm.myturn = false;




            vm._movedTo = _.where(vm.pieces, {position: position})[0];
            vm._movedTo.queen = vm.selectedpiece.queen;

            if(vm.game.player ==='player1'){
                vm._movedTo.class=vm.selectedpiece.class;

                if(!vm._movedTo.queen){
                    var atAway = vm.player2Home.indexOf(position);

                    if(atAway >= 0){
                        vm._movedTo.queen = true;
                        vm._movedTo.class=vm.icons.player1Queen;
                    }
                }
            }
            else{
                vm._movedTo.class=vm.selectedpiece.class;
                if(!vm._movedTo.queen){
                    var atAway = vm.player1Home.indexOf(position);
                    if(atAway >= 0){
                        vm._movedTo.queen = true;
                        vm._movedTo.class=vm.icons.player2Queen;
                    }
                }
            }
            vm._movedTo.player=vm.game.player;
            vm.selectedpiece.class= vm.icons.blank;
            vm.selectedpiece.player=undefined;
            vm.selectedpiece.queen = false;
vm._movedTo, player: vm.game.player, name: vm.game.name});
            socket.emit('move taken', {piece: vm.selectedpiece, movedTo: vm._movedTo, player: vm.game.player, name: vm.game.name});
        }

The last thing to do is send the move message to the server.

Whooph! That was the longest blog to date but I hope you found it interesting!

The git repository is here and should be available for you to fork.