Putting it all together – Tic-tac-toe

Before we go crazy with our new knowledge about networking, WebSockets, and multiplayer game architecture, let us apply these principles in the simplest way possible by creating a very exciting networked game of Tic-tac-toe. We will use plain WebSockets to communicate with the server, which we'll write in pure JavaScript. Since this JavaScript is going to be run in a server environment, we will use Node.js (refer to https://nodejs.org/), which you may or may not be familiar with at this point. Do not worry too much about the details specific to Node.js just yet. We have dedicated a whole chapter just to getting started with Node.js and its associated ecosystem. For now, try to focus on the networking aspects of this game.

Putting it all together – Tic-tac-toe

Surely, you are familiar with Tic-tac-toe. Two players take turns marking a single square on a 9x9 grid, and whoever marks three spaces on the board with the same mark such that a straight line is formed either horizontally, vertically, or diagonally wins. If all nine squares are marked and the previously mentioned rule is not fulfilled, then the game ends in a draw.

Node.js – the center of the universe

As promised, we will discuss Node.js in great depth in the next chapter. For now, just know that Node.js is a fundamental part of our development strategy since the entire server will be written in Node, and all other supporting tools will take advantage of Node's environment. The setup that we'll use for this first demo game contains three main parts, namely, the web server, the game server, and the client files (where the game client resides).

Node.js – the center of the universe

There are six main files that we need to worry about for now. The rest of them are automatically generated by Node.js and related tooling. As for our six scripts, this is what each of them does.

The /Player.js class

This is a very simple class that is intended mostly to describe what is expected by both the game client and the server.

/**
 *
 * @param {number} id
 * @param {string} label
 * @param {string} name
 * @constructor
 */
var Player = function(id, label, name) {
    this.id = id;
    this.label = label;
    this.name = name;
};

module.exports = Player;

The last line will be explained in more detail when we talk about the basics of Node.js. For now, what you need to know it is that it makes the Player class available to the server code as well as the client code that is sent to the browser.

In addition, we could very well just use an object literal throughout the game in order to represent what we're abstracting away as a player object. We could even use an array with those three values, where the order of each element would represent what the element is. While we're at it, we could even use a comma-separated string to represent all the three values.

As you can see, the slight verbosity incurred here by creating a whole new class just to store three simple values makes it easier to read the code, as we now know the contract that is established by the game when it asks for a Player. It will expect attributes named id, label, and name to be present there.

In this case, id can be considered a bit superfluous because its only purpose is to identify and distinguish between the players. The important thing is that the two players have a unique ID. The label attribute is what each player will print on the board, which just happens to be a unique value as well between both the players. Finally, the name attribute is used to print the name of each player in a human-readable way.

The /BoardServer.js class

This class abstracts a representation of the game of Tic-tac-toe, defining an interface where we can create and manage a game world with two players and a board.

var EventEmitter = require('events').EventEmitter;
var util = require('util'),

/**
 *
 * @constructor
 */
var Board = function() {
    this.cells = [];
    this.players = [];
    this.currentTurn = 0;
    this.ready = false;

    this.init();
};

Board.events = {
    PLAYER_CONNECTED: 'playerConnected',
    GAME_READY: 'gameReady',
    CELL_MARKED: 'cellMarked',
    CHANGE_TURN: 'changeTurn',
    WINNER: 'winner',
    DRAW: 'draw'
};

util.inherits(Board, EventEmitter);

As this code is intended to run in the server only, it takes full advantage of Node.js. The first part of the script imports two core Node.js modules that we'll leverage instead of reinventing the wheel. The first, EventEmitter, will allow us to broadcast events about our game as they take place. Second, we import a utility class that lets us easily leverage object-oriented programming. Finally, we define some static variables related to the Board class in order to simplify event registration and propagation.

Board.prototype.mark = function(cellId) {
    // …
    if (this.checkWinner()) {
        this.emit(Board.events.WINNER, {player: this.players[this.currentTurn]});
    }
};

The Board class exposes several methods that a driver application can call in order to input data into it, and it emits events when certain situations occur. As illustrated in the method mentioned previously, whenever a player successfully marks an available square on the board, the game broadcasts that event so that the driver program knows what has happened in the game; it can then contact each client through their corresponding sockets, and let them know what happened.

The /server.js class

Here, we have the driver program that uses the Board class that we described previously in order to enforce the game's rules. It also uses WebSockets to maintain connected clients and handle their individual interaction with the game.

var WebSocketServer = require('ws').Server;
var Board = require('./BoardServer'),
var Player = require('./Player'),

var PORT = 2667;
var wss = new WebSocketServer({port: PORT});
var board = new Board();

var events = {
    incoming: {
        JOIN_GAME: 'csJoinGame',
        MARK: 'csMark',
        QUIT: 'csQuit'
    },
    outgoing: {
        JOIN_GAME: 'scJoinGame',
        MARK: 'scMark',
        SET_TURN: 'scSetTurn',
        OPPONENT_READY: 'scOpponentReady',
        GAME_OVER: 'scGameOver',
        ERROR: 'scError',
        QUIT: 'scQuit'
    }
};

/**
 *
 * @param action
 * @param data
 * @returns {*}
 */
function makeMessage(action, data) {
    var resp = {
        action: action,
        data: data
    };

    return JSON.stringify(resp);
}

console.log('Listening on port %d', PORT);

The first part of this Node.js server script imports both our custom classes (Board and Player) as well as a handy third-party library called ws that helps us implement the WebSocket server. This library handles things such as the setup of the initial connection, the protocol upgrade, and so on, since these steps are not included in the JavaScript WebSocket object, which is only intended to be used as a client. After a couple of convenience objects, we have a working server that waits for connections on ws://localhost:2667.

wss.on('connection', function connection(ws) {
    board.on(Board.events.PLAYER_CONNECTED, function(player) {
        wss.clients.forEach(function(client) {
            board.players.forEach(function(player) {
                client.send(makeMessage(events.outgoing.JOIN_GAME, player));
            });
        });
    });

    ws.on('message', function incoming(msg) {
        try {
            var msg = JSON.parse(msg);
        } catch (error) {
            ws.send(makeMessage(events.outgoing.ERROR, 'Invalid action'));
            return;
        }

        try {
            switch (msg.action) {
                case events.incoming.JOIN_GAME:
                    var player = new Player(board.players.length + 1, board.players.length === 0 ? 'X' : 'O', msg.data);
                    board.addPlayer(player);
                    break;
                // ...
            }
        } catch (error) {
            ws.send(makeMessage(events.outgoing.ERROR, error.message));
        }
    });
});

The rest of the important stuff with this server happens in the middle. For brevity, we've only included one example of each situation, which includes an event handler registration for events emitted by the Board class as well as registration of a callback function for events received by the socket. (Did you recognize the ws.on('message', function(msg){}) function call? This is Node's equivalent of the client-side JavaScript socket.onmessage = function(event){} that we discussed earlier.)

Of major importance here is the way we handle incoming messages from the game clients. Since the client can only send us a single string as the message, how are we to know what the message is? Since there are many types of messages that the client can send to the server, what we do here is create our own little protocol. That is, each message will be a serialized JSON object (also known as an object literal) with two attributes. The first will be keyed with the value of action and the second will have a key of data, which can have a different value depending on the specified action. From here, we can look at the value of msg.action and respond to it accordingly.

For example, whenever a client connects to the game server, it sends a message with the following value:

{
    action: events.outgoing.JOIN_GAME,
    data: "<player nickname>"
};

Once the server receives that object as the payload of the onmessage event, it can know what the message means and the expected value for the player's nickname.

The /public/js/Board.js class

This class is very similar to BoardServer.js, with the main difference being that it also handles the DOM (meaning the HTML elements rendered and managed by the browser), since the game needs to be rendered to human players.

/**
 *
 * @constructor
 */
var Board = function(scoreBoard) {
    this.cells = [];
    this.dom = document.createElement('table'),
    this.dom.addEventListener('click', this.mark.bind(this));
    this.players = [];
    this.currentTurn = 0;
    this.ready = false;

    this.scoreBoard = scoreBoard;

    this.init();
};

Board.prototype.bindTo = function(container) {
    container.appendChild(this.dom);
};

Board.prototype.doWinner = function(pos) {
    this.disableAll();
    this.highlightCells(pos);
};

Again, for brevity, we have chosen not to display much of the game's logic. The important things to note here are that this version of the Board class is very much DOM-aware, and it behaves very passively to game decisions and the enforcement of the game's rules. Since we're using an authoritative server, this class does whatever the server tells it to, such as marking itself in a way that indicates that a certain participant has won the game.

The /public/js/app.js class

Similar to server.js, this script is the driver program for our game. It does two things: it takes input from the user with which it drives the server, and it uses input that it receives from the server in order to drive the board.

var socket = new WebSocket('ws://localhost:2667'),

var scoreBoard = [
    document.querySelector('#p1Score'),
    document.querySelector('#p2Score')
];

var hero = {};
var board = new Board(scoreBoard);

board.onMark = function(cellId){
    socket.send(makeMessage(events.outgoing.MARK, {playerId: hero.id, cellId: cellId}));
};

socket.onmessage = function(event){
    var msg = JSON.parse(event.data);

    switch (msg.action) {
        case events.incoming.GAME_OVER:
            if (msg.data.player) {
                board.doWinner(msg.data.pos);
            } else {
                board.doDraw();
            }

            socket.send(makeMessage(events.outgoing.QUIT, hero.id));
            break;

        case events.incoming.QUIT:
            socket.close();
            break;
    }
};

socket.onopen = function(event) {
    startBtn.removeAttribute('disabled'),
    nameInput.removeAttribute('disabled'),
    nameInput.removeAttribute('placeholder'),
    nameInput.focus();
};

Again, it is noteworthy how DOM-centric the client server is. Observe also how obedient the client is to the messages received from the server. If the action specified by the server in the message that it sends to the clients is GAME_OVER, the client cleans things up, tells the player that the game is over either because someone won the game or the game ended in a draw, then it tells the server that it is ready to disconnect. Again, the client waits for the server to tell it what to do next. In this case, it waits for the server to clean up, then tells the client to disconnect itself.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.12.207