A simple multi-player online game (HTML5 + node.js) - Part IV

Thursday, July 16, 2015

This is the fourth (and last) part of series where I describe what it took me to build a simple multiplayer online game ([part 1](http://www.valyouw.com/2015/07/a-simple-multi-player-online-game-html5_77.html){_blank}, [part 2](http://www.valyouw.com/2015/07/a-simple-multi-player-online-game-html5_55.html){_blank}, [part 3](http://www.valyouw.com/2015/07/a-simple-multi-player-online-game-html5.html){_blank}).

## Intro
In this section we are going to explore the server code, the main pares are:
  1. server.js - The entry point for the server, responsible for serving static files and accepting WebSockets
  2. lobby.js - Responsible for pairing players into matches
  3. game/ - All the snake game logic sits under this folder
## Server
As stated above, `server.js` is responsible for accepting connections and serving static files, I am not using any framework here but I do use the [ws](https://github.com/websockets/ws){_blank} module for handling WebSockets connections.

### Requests handlers
In the code below we create a new http server and pass a request listener callback to handle the request, quite a straight forward code:
```js
var http = require('http');
var server = http.createServer(function(req, res) {
    // This is a simple server, support only GET methods
    if (req.method !== 'GET') {
        res.writeHead(404);
        res.end();
        return;
    }

    // Handle the favicon (we don't have any)
    if (req.url === '/favicon.ico') {
        res.writeHead(204);
        res.end();
        return;
    }

    // This request if for a file
    var file = path.join(DEPLOY_DIR, req.url);
    serveStatic(res, file);
});
```

### Static files handler
Whenever we receive a GET request (which is not the favicon) we assume it is for a file, the `serveStatic` method will look for the file and stream it back to the client.
In the code I use 2 constant variables that helps with finding the files, the first is `DEPLOY_DIR` which is actually the root folder where the static files are, and the second is `DEFAULT_FILE` which is the name of the file that should be served if the request url points to a folder (of course that in real projects these 2 should be in some config file and not hard-coded like this)
```js
var DEPLOY_DIR = path.resolve(__dirname, '../client/deploy');
var DEFAULT_FILE = 'index.html';
```
So assume we deployed the project under `/var/www/SnakeMatch`, then `DEPLOY_DIR` is `/var/www/SnakeMatch/client/deploy`, and a request to `/all.js` will serve `/var/www/SnakeMatch/client/deploy/all.js`.

Here is the code of the `serveStatic` method, where `fs` is Node's fs module:
```fs
/**
* Serves a static file
* @param {object} res - The response object
* @param {string} file - The requested file path
*/
function serveStatic(res, file) {
    // Get the file statistics
    fs.lstat(file, function(err, stat) {
        // If err probably file does not exist
        if (err) {
            res.writeHead(404);
            res.end();
            return;
        }

        // If this is a directory we will try to serve the default file
        if (stat.isDirectory()) {
            var defaultFile = path.join(file, DEFAULT_FILE);
            serveStatic(res, defaultFile);
        } else {
            // Pipe the file over to the response
            fs.createReadStream(file).pipe(res);
        }
    });
}
```

### Accepting connections
After creating http server we need to bind on a port, we are using the `PORT` environment variable (to be used in Heroku) and defaults to 3000, for WebSockets we use `ws`, whenever we get a WebSocket connection we just send it to the lobby
```js
var WebSocketServer = require('ws').Server;
var port = process.env.PORT || 3000;
server.listen(port, function () {
    console.log('Server listening on port:', port);
});

// Create the WebSocket server (it will handle "upgrade" requests)
var wss = new WebSocketServer({server: server});
wss.on('connection', function(ws) {
    lobby.add(ws);
});
```

## Lobby
The Lobby is responsible for accepting new players, and pairing players into matches.
Whenever a new socket is added to the lobby it first creates a `Player` object (werapper around the socket, more on this later) and listen to its `disconnect` event, then it tries to pair it with another player into a `Match`, if there are no available players it puts the player in the `pendingPlayers` dictionary, if it succeeded to pair this player with another player the Match object is put in the `activeMatches` dictionary and it registers to the Match's `GameOver` event.
```js
Lobby.add = function (socket) {
    // Create a new Player, add it to the pending players dictionary and register to its disconnect event
    var player = new Player(socket);
    pendingPlayers[player.id] = player;
    player.on(Player.Events.Disconnect, Lobby.onPlayerDisconnect);

    // Try to pair this player with other pending players, if success we get a "match"
    var match = this.matchPlayers(player);
    if (match) {
        // Register the Match GameOver event and store the match in the active matches dictionary
        match.on(Match.Events.GameOver, Lobby.onGameOver);
        activeMatches[match.id] = match;

        // Remove the players in the match from the pending players
        delete pendingPlayers[match.player1.id];
        delete pendingPlayers[match.player2.id];

        // Start the match
        match.start();
    } else {
        // No match found for this player, let him know he is Pending
        player.send(protocol.buildPending());
    }
};
```
The rest of the code in the Lobby is not that interesting, `matchPlayers` just loops over the `pendingPlayers` dictionary and returns a new `Match` object if it found another pending player (which is not the current player). When a match is over (`GameOver` event) we just disconnect the two players (which will close their sockets), and delete the match from the `activeMatches` dictionary.

## The Game
Now we will go over the code under the `server/game` folder, it contains the `Player`, `Match` and `SnakeEngine` classes.

### Player class
The Player is just a wrapper around the socket class, whenever new data arrives on the socket it raises a `message` event, if the socket gets closed it raises a `disconnect` event, and it exposes a `send` method which is used to write data over the socket. Below is the ctor and send methods:
```js
var Emitter = require('events').EventEmitter,
    util = require('util'),
    uuid = require('node-uuid');

function Player(socket) {
    // Make sure we got a socket
    if (typeof socket !== 'object' || socket === null) {
        throw new Error('socket is mandatory');
    }

    Emitter.call(this);

    this.id = uuid.v1();
    this.index = 0; // The player index within the game (will be set by the Match class)
    this.online = true;
    this.socket = socket;

    // Register to the socket events
    socket.on('close', this.onDisconnect.bind(this));
    socket.on('error', this.onDisconnect.bind(this));
    socket.on('message', this.onMessage.bind(this));
}
util.inherits(Player, Emitter);

Player.prototype.send = function(msg) {
    if (!msg || !this.online) {
        return;
    }

    try {
        this.socket.send(msg);
    } catch (ignore) {}
};
```

### Match class
This class is responsible for all the game logistics, it updates the snake-engine every 100 msec, it sends updates to the clients, it read messages from the client etc.
NOTE: the Match class doesn't know how to "play" snake, that's why we have the snake-engine for.
Although we described it on the first post lets go over the course of a snake match: start by sending a `Ready` message to the clients with all the game info (board size, snakes initial position etc), then there are 3 `Steady` messages (evet 1 second), then there is a `go` message signaling to the clients that the game has started, then a series of `Update` messages are being sent every 100 milliseconds, and finally there is a `GameOver` message.
The match is over if when one of the players has failed or 60 seconds has passed, if after 60 seconds the score is tied there is an overtime of 10 seconds until one player wins.
Now lets see how the Match class is doing all this, first we define some constants:
```js
var MATCH_TIME = 60000; // In milliseconds
var MATCH_EXTENSION_TIME = 10000; // In milliseconds
var UPD_FREQ = 100;
var STEADY_WAIT = 3; // number of steady messages to send
var BOARD_SIZE = {
    WIDTH: 500,
    HEIGHT: 500,
    BOX: 10
};
```
In the ctor we initialize the game, note that each player is assigned to an index (player1 / player2).
```js
function Match(player1, player2) {
    Emitter.call(this);
    this.id = uuid.v1();
    this.gameTimer = null;
    this.matchTime = MATCH_TIME; // The match timer (each match is for MATCH_TIME milliseconds)

    // Set the players indexes
    this.player1 = player1;
    this.player1.index = 1;
    this.player2 = player2;
    this.player2.index = 2;

    // Register to the players events
    this.player1.on(Player.Events.Disconnect, this.onPlayerDisconnect.bind(this));
    this.player2.on(Player.Events.Disconnect, this.onPlayerDisconnect.bind(this));

    this.player1.on(Player.Events.Message, this.onPlayerMessage.bind(this));
    this.player2.on(Player.Events.Message, this.onPlayerMessage.bind(this));

    // Create the snake game
    this.snakeEngine = new SnakeEngine(BOARD_SIZE.WIDTH, BOARD_SIZE.HEIGHT, BOARD_SIZE.BOX);
}
```

### Ready-Steady-Go
The ready-steady-go flow happens in the `start` and `steady` methods:
```js
Match.prototype.start = function() {
    // Build the ready message for each player
    var msg = protocol.buildReady(this.player1.index, this.snakeEngine.board, this.snakeEngine.snake1, this.snakeEngine.snake2);
    this.player1.send(msg);

    msg = protocol.buildReady(this.player2.index, this.snakeEngine.board, this.snakeEngine.snake1, this.snakeEngine.snake2);
    this.player2.send(msg);

    // Start the steady count down
    this.steady(STEADY_WAIT);
};

/**
 * Handles the steady count down
 * @param {number} steadyLeft - The number of steady events left
 */
Match.prototype.steady = function(steadyLeft) {
    var msg;

    // Check if steady count down finished
    if (steadyLeft === 0) {
        // Send the players a "Go" message
        msg = protocol.buildGo();
        this.player1.send(msg);
        this.player2.send(msg);

        // Starts the update events (this is the actual game)
        this.gameTimer = setTimeout(this.update.bind(this), UPD_FREQ);
        return;
    }

    // Sends the players another steady message and call this method again in 1 sec
    msg = protocol.buildSteady(steadyLeft);
    this.player1.send(msg);
    this.player2.send(msg);
    --steadyLeft;
    this.gameTimer = setTimeout(this.steady.bind(this, steadyLeft), 1000);
};
```

### Update cycle
The `update` method is being called every 100 milliseconds, the method is quite self-explanatory but do note that `snakeEngine.update()` returns a result object with info about the game state, more specifically, it tells us whether one snake has lost (by colliding into itself/border) and if there was a change to the pellets (removed/added).
```js
Match.prototype.update = function() {
    // Update the match time, this is not super precise as the "setTimeout" time is not guaranteed,
    // but ok for our purposes...
    this.matchTime -= UPD_FREQ;

    // Update the game
    var res = this.snakeEngine.update();

    // If no snake lost on this update and there is more time we just reload the update timer
    if (res.loosingSnake < 0 && this.matchTime > 0) {
        this.gameTimer = setTimeout(this.update.bind(this), UPD_FREQ);
        this.sendUpdateMessage(res);
        return;
    }

    var msg;
    // If no snake lost it means time's up, lets see who won.
    if (res.loosingSnake < 0) {
        // Check if there is a tie
        if (this.snakeEngine.snake1.parts.length === this.snakeEngine.snake2.parts.length) {
            // We don't like ties, lets add more time to the game
            this.matchTime += MATCH_EXTENSION_TIME;
            this.gameTimer = setTimeout(this.update.bind(this), UPD_FREQ);
            this.sendUpdateMessage(res);
            return;
        }

        // No tie, build a GameOver message (the client will find which player won)
        msg = protocol.buildGameOver(protocol.GameOverReason.End, null, this.snakeEngine.snake1, this.snakeEngine.snake2);
    } else {
        // Ok, some snake had a collision and lost, since we have only 2 players we can easily find the winning snake
        var winningPlayer = (res.loosingSnake + 2) % 2 + 1;
        msg = protocol.buildGameOver(protocol.GameOverReason.Collision, winningPlayer);
    }

    // Send the message to the players and raise the GameOver event
    this.player1.send(msg);
    this.player2.send(msg);

    this.emit(Match.Events.GameOver, this);
};
```

### Handling clients messages
Whenever the client sends a message it first get parsed using the Protocol object, then if it is a `ChangeDirection` request we pass it to the snake-engine for processing, note that we put the player index on the message so that snake-engine would know what player to update.
```js
Match.prototype.onPlayerMessage = function(player, msg) {
    // Parse the message
    var message = protocol.parseMessage(msg);
    if (!message) {
        return;
    }

    switch (message.type) {
        case protocol.Messages.ChangeDirection:
            message.playerIndex = player.index;
            this.snakeEngine.handleDirChangeMessage(message);
            break;
    }
};
```
That's it for the Match class, the rest of the code is not that interesting.

### Snake Engine
The snake-engine is responsible for "playing" the snake game, on every `update` it checks whether a snake had collided with itself, went out-of-bounds, ate a pellet etc.
In the ctor we create the 2 snake objects, both snakes are created at the first row of the board, one is created on the left side and the other is created on the right side. Remember that the Board is divided into boxes, and that `Board.toScreen()` gets a box index and returns the screen x/y.
```js
function SnakeEngine(width, height, boxSize) {
    this.board = new Board(width, height, boxSize);

    // The first snake is created on the left side and is heading right (very top row, y index = 0)
    var snakeLoc = this.board.toScreen(INITIAL_SNAKE_SIZE - 1);
    this.snake1 = new Snake(snakeLoc.x, snakeLoc.y, boxSize, INITIAL_SNAKE_SIZE, protocol.Direction.Right);

    // The second snake is created on the right side and is heading left (very top row, y index = 0)
    snakeLoc = this.board.toScreen(this.board.horizontalBoxes - INITIAL_SNAKE_SIZE);
    this.snake2 = new Snake(snakeLoc.x, snakeLoc.y, boxSize, INITIAL_SNAKE_SIZE, protocol.Direction.Left);

    /** @type {Pellet[]} */
    this.pellets = [];
}
```
The interesting methods are `update`, `checkCollision` and `addPellet`.
In the update method we do the following for each snake: call the snake update method (tell it to move to its next location), check for collisions, check if it ate a pellet. If there was a collision we stop immediately as the game has over, if there was no collision we try to add a new pellet to the game.
```js
SnakeEngine.prototype.update = function() {
    var res = new GameUpdateData();

    // Update snake1
    this.snake1.update();

    // Check if the snake collides with itself or out-of-bounds
    var collision = this.checkCollision(this.snake1);
    if (collision) {
        res.loosingSnake = 1;
        return res;
    }

    // Check if the snake eats a pellet
    res.pelletsUpdate = this.eatPellet(this.snake1);

    // Update snake2
    this.snake2.update();

    // Check if the snake collides with itself or out-of-bounds
    collision = this.checkCollision(this.snake2);
    if (collision) {
        res.loosingSnake = 2;
        return res;
    }

    // Check if the snake eats a pellet
    res.pelletsUpdate = this.eatPellet(this.snake2) || res.pelletsUpdate;

    // Finally add new pellet
    res.pelletsUpdate = this.addPellet() || res.pelletsUpdate;

    // No one lost (yet...).
    return res;
};
```
In `checkCollision` we first check if the snake went out-of-bounds, we do this by comparing the snake's head to the board dimensions. Remember that the snake head is a rectangle, where the upper-left corner is denoted by x/y, so when we want to check if the snake crossed the top/left border we use x/y, but when we want to check whether the snake crossed the bottom/right border we use the bottom-right corner of the snake head.
Checking whether the snake had collided with itself is quite simple, just loop thru all the snake parts (excluding the head), and check whether they are equal to the head (equals just check x/y).
```js
SnakeEngine.prototype.checkCollision = function(snake) {
    // Check if the head is out-of-bounds
    if (snake.parts[0].location.x < 0 ||
        snake.parts[0].location.y < 0 ||
        snake.parts[0].location.x + snake.parts[0].size > this.board.rectangle.width ||
        snake.parts[0].location.y + snake.parts[0].size > this.board.rectangle.height) {
            return true;
    }

    // Check if the snake head collides with its body
    for (var i = 1; i < snake.parts.length; ++i) {
        if (snake.parts[0].location.equals(snake.parts[i].location)) {
            return true;
        }
    }

    return false;
};
```
### Adding pellets
When we come to add a new pellet to the game we first check that we have not exceeded the maximum number of allowed pellets, then we select a random box on the board and check that the box is vacant. Since `addPellet` is getting called quite frequently (every update cycle) we have to do some filtering as we want the pellets to be added on a random timing, so at the very beginning of the method we check if `Math.random() > 0.2`, if yes we immediately return without adding anything, so on average we would drop 8 of 10 calls.
```js
SnakeEngine.prototype.addPellet = function() {
    // Check if we should add pellets
    if (this.pellets.length >= MAX_PELLETS || Math.random() > 0.2) {
        return false;
    }

    // Keep loop until we found a spot for a pellet (theoretically this can turn into an infinite loop, so a solution could
    // be to stop the random search after X times and look for a spot on the board).
    var keepSearch = true;
    while (keepSearch) {
        keepSearch = false;

        // Take a random spot on the board
        var boxIndex = Math.floor(Math.random() * this.board.horizontalBoxes * this.board.horizontalBoxes);
        var loc = this.board.toScreen(boxIndex);

        // check that this spot is not on snake1
        for (var i = 0; i < this.snake1.parts.length; ++i) {
            if (this.snake1.parts[i].location.equals(loc)) {
                keepSearch = true;
                break;
            }
        }

        if (!keepSearch) {
            // check that this spot is not on snake2
            for (i = 0; i < this.snake2.parts.length; ++i) {
                if (this.snake2.parts[i].location.equals(loc)) {
                    keepSearch = true;
                    break;
                }
            }
        }

        if (!keepSearch) {
            // check that this spot is not on existing pellet
            for (i = 0; i < this.pellets.length; ++i) {
                if (this.pellets[i].location.equals(loc)) {
                    keepSearch = true;
                    break;
                }
            }
        }

        if (!keepSearch) {
            // Hooray we can add the pellet
            this.pellets.push(new Pellet(loc));
        }
    }

    return true;
};
```

## THE END
Pshew... if you have made it all the way to here, well done and thank you!
I hope this series was in some of interest to you, to me it was fun programming this game, feel free to explore the [code](https://github.com/ValYouW/SnakeMatch){_blankl} and even make it better !!

1 comment :