📜 ⬆️ ⬇️

Writing an online game on NodeJS, Express and Socket.IO

Hi% habraname%!





*** This material contains logical errors in the game itself, but this does not affect the technical content of the article, the purpose of which is not to play, but to figure out how to work with the tools indicated in the title. Continued. We bring the game to working condition, taking into account all the errors described in the comments

')
Few people today can say they don’t know about NodeJS, lately they talk and write a lot about it.
I started my journey of acquaintance with NodeJS six months ago, then for me it was just interesting and new, I couldn’t have thought that in six months it would become my main development tool.

Since all the training material is either an article about asynchrony, or how to write your server or chat, I did not find anything interesting for myself in the training material. He wrote on the sly different small applications that partially replaced the background work of php in different projects.

But now I feel strong enough to write a full-fledged training and not dull material from a beginner to a real working application. This is not just an application, but an online game using the most popular tools Express and Socket.IO, yes, yes, a multiplayer that any average statistical js developer can make.

The fact that such Express and Socket.IO have already been written in many places, so I will not describe again, paying more attention to the development process.

For a start, I wanted to choose the good old tanchiki and I didn’t choose well, it would be sad to write it second in Habré :)
I decided not to complicate the process of developing graphics and take a simple game, so my choice fell on tic-tac-toe, but to complicate my task, it was decided to make it universally, with the ability to set any size of the playing field and any number of moves to win.

And so it is decided! I start to do tic-tac-toe.

We will determine the structure of the future game, what we need as a result of:


I started the development as usual with the interface. I chose the jQuery framework and jQueryUI respectively.

Creating a page and connecting the necessary styles and libraries jquery, jqueryUI:
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/vader/jquery-ui.css"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script> 

The interface is a status line, a sidebar of statistics and the playing field itself, everything is done with simple tables:
 <table border="0" width="100%"> <thead> <th colspan="2" id="status" class="ui-widget ui-state-hover ui-corner-all">  ...</th> </thead> <tbody> <td id="stats" class="ui-widget" valign="top"><br /><button id="reload"> </button><br /><br /></td> <td id="board" class="ui-widget" valign="top"><div id="masked" class="ui-widget-shadow ui-corner-all ui-widget-overlay"></div> <table class="ui-widget ui-corner-all" cellpadding="0" cellspacing="0" align="left" id="board-table"></table> </td> </tbody> </table> 


I think there is nothing to comment on, everything is more than clear. Next, CSS was quickly sketched and I started writing client event handling.

All the code tried to write as clean and logical as possible with the expectation that this is educational material that should teach only good things even in small things :)

A game object has been created:
 var TicTacToe = { gameId: null, //      ID . turn: null, //    ,   X  O i: false, //   ,    init: function() { ... }, //         startGame: function (gameId, turn, x, y) { ... }, //         mask: function(state) { ... }, //    ,            :) move: function (id, turn, win) { ... }, //      endGame: function (turn, win) { ... } //  ,   } 


Now we will consider each function in turn, commented as much as possible, and so Init ():
 $(function() { // UI    $('#reload').button({icons:{primary:'ui-icon-refresh'}}).click(function(){window.location.reload();}); //    nodejs  socket.io var socket = io.connect(window.location.hostname + ':1337', {resource: 'api'}); //   event' ()   socket.io //  socket.on('connect', function () { $('#status').html('    '); }); //  socket.on('reconnect', function () { $('#connect-status').html(',  '); }); //   socket.on('reconnecting', function () { $('#status').html('   , ...'); }); //  socket.on('error', function (e) { $('#status').html(': ' + (e ? e : ' ')); }); //        //   socket.on('wait', function(){ $('#status').append('...  ...'); }); //   socket.on('exit', function() { //           TicTacToe.endGame(TicTacToe.turn, 'exit'); }); //    ,   //  ID ,        xy socket.on('ready', function(gameId, turn, x, y) { $('#status').html('   !  ! ' + (turn == 'X' ? '   ' : '  ') + '!'); //        TicTacToe.startGame(gameId, turn, x, y); //     ,    :) $('#stats').append($('<div/>').attr('class', 'turn ui-state-hover ui-corner-all').html(' : <b>' + (turn=='X'?'':'') + '</b>')); //       ,   $("#board-table td").click(function (e) { //  ,      ID   ID  ,    XxY if(TicTacToe.i) socket.emit('step', TicTacToe.gameId, e.target.id); //        }).hover(function(){ $(this).toggleClass('ui-state-hover'); }, function(){ $(this).toggleClass('ui-state-hover'); }); }); //   socket.on('step', function(id, turn, win) { //  ID ,      . win          TicTacToe.move(id, turn, win); }); //  socket.on('stats', function (arr) { var stats = $('#stats'); stats.find('div').not('.turn').remove(); for(val in arr) { stats.prepend($('<div/>').attr('class', 'ui-state-hover ui-corner-all').html(arr[val])); } }); }); 


Now let's take a closer look at how the game starts:
 startGame: function (gameId, turn, x, y) { //  ID  this.gameId = gameId; //    this.turn = turn; //   ,  X        :) this.i = (turn == 'X'); //    var table = $('#board-table').empty(); //      for(var i = 1; i <= y; i++) { var tr = $('<tr/>'); for(var j = 0; j < x; j++) { //        ID  X  Y  (id="2x3") tr.append($('<td/>').attr('id', (j+1) + 'x' + i).addClass('ui-state-default').html(' ')); } table.append(tr); } //   $("#board").show(); //     ,   this.mask(!this.i); }, 


I will not describe the function of the mask, everything is trite there.
Further function get the player's turn:
 move: function (id, turn, win) { //  : ID    ,  ,     this.i = (turn != this.turn); //    $("#" + id).attr('class', 'ui-state-hover').html(turn); //     if (!win) { //   ,   this.mask(!this.i); //        $('#status').html(' ' + (this.i ? ' ' : ' ')); //   } else { this.endGame(turn, win); //    ,   } }, 


Completion of the game:
 endGame: function (turn, win) { //  :   ,    var text = ''; //     3  switch(win) { case 'none': text = '!'; break; //     case 'exit': text = '    !  '; break; //    default: text = ' ' + (this.i ? '! =(' : '! =)'); //   } //        $("<div/>").html(text).dialog({ title: ' ', modal: true, closeOnEscape: false, resizable: false, buttons: { "  ": function() { $(this).dialog("close"); window.location.reload(); }}, close: function() { window.location.reload(); } }); } 


That's all! All created files put in a new folder named public.

Was the client part too easy? The server part is a bit more complicated!


Install the necessary modules:
 npm install express npm install socket.io 


Create a starting index.js file, so far simple, let's deal with it:
 //    var express = require('express'), socketio = require('socket.io'); //      express var app = express.createServer(); //        express  //      ,      var io = socketio.listen(app); //   express    app.use(express.static(__dirname + '/public')); //       app.listen(80); //     socket.io       3,     io.set('log level', 3); //     ,     /socket.io io.set('resource', '/api'); 


Now we write the same thing but without comments and in a simplified form, so cleaner:
 var express = require('express'), app = express.createServer(), io = require('socket.io').listen(app), TicTacToe = require('./models/tictactoe'); app.use(express.static(__dirname + '/public')); app.listen(1337); io.set('log level', 1); io.set('resource', '/api'); 


If you noticed here I also added a model
 TicTacToe = require('./models/tictactoe'); 

we will write it now, create a folder next to index.js with the name models and create a file tictactoe.js in it
This is a regular module in nodejs and will be used by the export function, it will be the heart of our game, all the logic.

Since we are writing an online game, we should initially have the architecture of the server application in the form of user objects, the game and their collections.

Create the main objects:
 //    ,  ,          var TicTacToe = module.exports = function() { //  id  =   this.games = []; //    = id  this.users = []; //        this.free = []; //   this.x = 6; this.y = 6; //    this.stepsToWin = 4; } //  ,       var GameItem = function(user, opponent, x, y, stepsToWin) { //        this.board[id  ] =   this.board = []; //  this.user = user; // X this.opponent = opponent; // O //   this.x = x; this.y = y; //    this.stepsToWin = stepsToWin; // -   this.steps = 0; } 


Thus, when our server is launched, it creates the TicTacToe game, and when users connect, we will create for them personal GameItem games within the TicTacToe collection, in which it will be seen who plays with whom and on what parameters.

Now consider the function of creating these same games in the collection:
 TicTacToe.prototype.start = function(user, cb) { //     -    //     Object.keys      if(Object.keys(this.free).length > 0) { //       ID var opponent = Object.keys(this.free).shift(); //      var game = new GameItem(user, opponent, this.x, this.y, this.stepsToWin); //   ID   ID  var id = user + opponent; //      this.games[id] = game; //       this.users[user] = id; //      this.users[opponent] = id; //  callback    cb(true, id, opponent, this.x, this.y); } else { //  ,    this.free[user] = true; //  callback    cb(false); } } 


As you can see, we use change through prototypes, so we increase our module with necessary functionality.
I will also use the callback function (callbacks) in the whole game. This gives us the opportunity to write asynchronous code.

I will quickly explain again why this is necessary on a simple example, if someone has not yet read one of the dozens of articles about asynchrony :)

So suppose the user has accessed the server and wants to start the game. The server is happy to answer ok, I start to start your game and does it like this:
 1. -->    2. TicTacToe.start(); 3. <--   


So in this case, after step 1, steps 2 and 3 will work synchronously, that is, at the same time, the user will receive an answer before the game is actually created. Therefore, we use the callback function, changing the logic as follows:

 1. -->    2. TicTacToe.start(function(){ 3. <--   }); 


Now, after step 1, we only run the function in step 2, and only after the callback does it work out the anonymous function and goes to step 3 to answer the result to the user.

Let's go back to our game. We taught her to determine the queue of gaming sites and connect pairs of users for the game.

Now consider how we will end the game:
 TicTacToe.prototype.end = function(user, cb) { //          delete this.free[user]; //      ,     if(this.users[user] === undefined) return; //  ID      var gameId = this.users[user]; //     ,  if(this.games[gameId] === undefined) return; //      ID var game = this.games[gameId]; //      var opponent = (user == game.user ? game.opponent : game.user); //    delete this.games[gameId]; //   game = null; //     delete this.users[user]; //  ID   ID     cb(gameId, opponent); } 


Now we will move on to the most interesting part, how tic tac toe works.

This time we will add functionality not only for the collection, but also for the game object itself, let's make a player's move:
 TicTacToe.prototype.step = function(gameId, x, y, user, cb) { //     proxy             this.games[gameId].step(x, y, user, cb); } GameItem.prototype.step = function(x, y, user, cb) { //        if(this.board[x + 'x' + y] !== undefined) return; //   X  Y    ,          this.board[x + 'x' + y] = this.getTurn(user); //     this.steps++; //            cb(this.checkWinner(x, y, this.getTurn(user)), this.getTurn(user)); } 

Let's stop and consider this function in more detail. There are calls to the other two functions in it, this.getTurn (). It is responsible for returning what the user who made the move goes to, passing the user ID to it, and here is the function itself that is added to the game object:
 GameItem.prototype.getTurn = function(user) { return (user == this.user ? 'X' : 'O'); } 


It's simple, if this is the user who created the game, then it is obvious that he is walking X, and if the opponent is O.

The second function called in callback is a check for the winner:
 GameItem.prototype.checkWinner = function(x, y, turn) { //   ,      if(this.steps == (this.x * this.y)) { //  return 'none'; //    } else if( //      this.checkWinnerDynamic('-', x, y, turn) || this.checkWinnerDynamic('|', x, y, turn) || this.checkWinnerDynamic('\\', x , y, turn) || this.checkWinnerDynamic('/', x, y, turn) ) { //   return true; } else { //   return false; } } 


We again added functionality to our game object. In the function, we again get the coordinates of the move and what went. Immediately check how many moves are made with it and see how many moves are available by counting it by multiplying the size of the field with each other.

If we still have free cells, then the game is not over yet, maybe there is a winner. Checking the winner is the most difficult function in the whole game, we will look at it a little lower. In cases where there is no winner, then return false.

Now about the check function on the winner, it has 4 parameters:
1 - Search algorithm, can have values ​​-, |, / and \ (yes, one backslash, because in the code escaping quotes) why icons you ask, it’s all very simple guides for checking, for clarity, I decided to use their.
2,3 - stroke coordinates
4 - what went

Now it’s a powerful and inimitable function, I advise you to look at it for the beginning and note that each case has almost a standard form, and what is the difference I’ll tell you after the code:
 GameItem.prototype.checkWinnerDynamic = function(a, x, y, turn) { //    4 : ,   2  //          ,,     4  var win = 1; switch(a) { //    case '-': var toLeft = toRight = true, min = x - this.stepsToWin, max = x + this.stepsToWin; min = (min < 1) ? 1 : min; max = (max > this.x) ? this.x : max; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toLeft && !toRight) return false; if(toLeft && min <= (xi) && this.board[(xi) + 'x' + y] == turn) { win++; } else { toLeft = false; } if(toRight && (x+i) <= max && this.board[(x+i) + 'x' + y] == turn) { win++; } else { toRight = false; } } break; //    case '|': var toUp = toDown = true, min = y - this.stepsToWin, max = y + this.stepsToWin; min = (min < 1) ? 1 : min; max = (max > this.y) ? this.y : max; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toUp && !toDown) return false; if(toUp && min <= (yi) && this.board[x + 'x' + (yi)] == turn) { win++; } else { toUp = false; } if(toDown && (y+i) <= max && this.board[x + 'x' + (y+i)] == turn) { win++; } else { toDown = false; } } break; //      case '\\': var toUpLeft = toDownRight = true, minX = x - this.stepsToWin, maxX = x + this.stepsToWin, minY = y - this.stepsToWin, maxY = y + this.stepsToWin; minX = (minX < 1) ? 1 : minX; maxX = (maxX > this.x) ? this.x : maxX; minY = (minY < 1) ? 1 : minY; maxY = (maxY > this.y) ? this.y : maxY; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toUpLeft && !toDownRight) return false; if(toUpLeft && minX <= (xi) && minY <= (yi) && this.board[(xi) + 'x' + (yi)] == turn) { win++; } else { toUpLeft = false; } if(toDownRight && (x+i) <= maxX && (y+i) <= maxY && this.board[(x+i) + 'x' + (y+i)] == turn) { win++; } else { toDownRight = false; } } break; //      case '/': var toDownLeft = toUpRight = true, minX = x - this.stepsToWin, maxX = x + this.stepsToWin, minY = y - this.stepsToWin, maxY = y + this.stepsToWin; minX = (minX < 1) ? 1 : minX; maxX = (maxX > this.x) ? this.x : maxX; minY = (minY < 1) ? 1 : minY; maxY = (maxY > this.y) ? this.y : maxY; for(var i = 1; i <= this.stepsToWin; i++) { if(win >= this.stepsToWin) return true; if(!toDownLeft && !toUpRight) return false; if(toDownLeft && minX <= (xi) && (y+i) <= maxY && this.board[(xi) + 'x' + (y+i)] == turn) { win++; } else { toDownLeft = false; } if(toUpRight && (x+i) <= maxX && (yi) <= maxY && this.board[(x+i) + 'x' + (yi)] == turn) { win++; } else { toUpRight = false; } } break; default: return false; break; } return(win >= this.stepsToWin); } 


Algorithmization


Each case is a separate check for different algorithms, but they all have one thing in common, this is a common shift in the game field from the current position and checking the values ​​of these fields.
Since I initially complicated my task and the game can have any field size, as well as any number of moves to win, we have a universal algorithm consisting of the following checks:



We have completed describing our game module. All features are ready! Now we will hang up the server handlers to interact with client handlers and look at all this in action.

Save the file and return to our main index.js, we will add the work with socket.io to it, add the necessary events, as well as common game variables:
 //    ,      ,        var countGames = onlinePlayers = onlineGames = 0, countPlayers = [], Game = new TicTacToe(); //   ,         Game.x = Game.y = 6; // Default: 6 //  -      Game.stepsToWin = 4; // Default: 4 //         io.sockets.on('connection', function (socket) { //          ID  IP  console.log('%s: %s - connected', socket.id.toString(), socket.handshake.address.address); //       stats       io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); //       5 ,    setInterval(function() { io.sockets.emit('stats', [ ' : ' + countGames, ' : ' + Object.keys(countPlayers).length, ' : ' + onlineGames, ' : ' + onlinePlayers ]); }, 5000); //    ,  ID     ID   md5  Game.start(socket.id.toString(), function(start, gameId, opponent, x, y){ //  callback'       ,      //       ,       null if(start) { //        ID  ID  //      socket.io socket.join(gameId); //   ()        io.sockets.socket(opponent).join(gameId); //          socket.emit('ready', gameId, 'X', x, y); //     io.sockets.socket(opponent).emit('ready', gameId, 'O', x, y); //          countGames++; onlineGames++; } else { //  ,      io.sockets.socket(socket.id).emit('wait'); } //        ip     if(countPlayers[socket.handshake.address.address] == undefined) countPlayers[socket.handshake.address.address] = true; //     onlinePlayers++; }); //     socket.on('step', function (gameId, id) { //   ID   XxY var coordinates = id.split('x'); //       ,   proxy        Game.step(gameId, parseInt(coordinates[0]), parseInt(coordinates[1]), socket.id.toString(), function(win, turn) { //     ,  ,             //       in()        io.sockets.in(gameId).emit('step', id, turn, win); //      if(win) { //      ,     Game.end(socket.id.toString(), function(gameId, opponent) { //          socket.leave(gameId); //     io.sockets.socket(opponent).leave(gameId); }); } }); }); //             socket.on('disconnect', function () { //     ,      //       ,   Game.end(socket.id.toString(), function(gameId, opponent) { //     ,    Game.end       , ID  io.sockets.socket(opponent).emit('exit'); //     socket.leave(gameId); //     io.sockets.socket(opponent).leave(gameId); //     onlineGames--; }); //    onlinePlayers--; //      console.log('%s: %s - disconnected', socket.id.toString(), socket.handshake.address.address); }); }); 


! NodeJS, , socket.io, express .

Now you can play, to start the game, you need to click "New game":
ivan.zhuravlev.name/game - 6x6 field with 4 moves to win
ivan.zhuravlev.name/game3 - 3x3 field with 3 moves to win
View sources: github.com/intech/TicTacToe
-, proxy nginx , 1337
, , 8 1 , :)
— : 12 22 .
: 3 ,

Statistics


CPU

Memory

google analytics .
30 50 , , .

Source: https://habr.com/ru/post/132544/


All Articles