Jogo da Velha com Node.js + Socket.io + MongoDB (Parte 2)

Na primeira parte deste tutorial criamos um jogo da velha em JavaScript, nesta segunda parte iremos modificá-lo para trabalhar com comunicação em tempo real com Node.js e Socket.IO e ao final salvar os dados do placar no banco. Vem comigo…

NPM e Socket.IO

Passamos pela parte de criação do servidor e temos nosso joguinho, mas e se estivéssemos falando de um jogo mais sofisticado, de uma outra aplicação qualquer como um chat ou um gráfico por exemplo, em que houvesse a necessidade de manter tudo sendo atualizado em tempo real. Bom é nessa parte onde entra nosso amigo Socket.IO, mas espere, antes de falarmos nele vamos falar sobre o tal NPM.

Node.js package manager ou NPM para os mais íntimos, é o gerenciador de pacotes do node. Um pacote é um arquivo ou um diretório que contém módulos, sim como aqueles que vimos no início do artigo. Alguns módulos já vem com o Node por padrão, mas outros como o Socket.IO temos que instalar, então para isso abra o terminal, acesse o diretório do nosso projeto e execute o comando “npm install socket.io”.

Após isso volte ao projeto e você verá que existe um novo diretório chamado node_modules e dentro dele uma série de dependências, além de um arquivo package-lock.json listando e descrevendo essas dependências, você pode ler mais sobre o propósito dele aqui https://docs.npmjs.com/files/package-lock.json (em inglês), caso tenha interesse.

Agora podemos voltar ao assunto do Socket. Socket.IO é uma biblioteca JavaScript que, como o próprio site oficial diz, “permite comunicação em tempo real, bidirecional e baseada em eventos”. Isso quer dizer que o cliente (seu navegador) pode se comunicar com o servidor e vice-versa em tempo real, sempre que um evento for disparado.

Vamos ver isso funcionando na prática.

Lidando com mensagens

Do lado do cliente temos que incluir a tag script com o caminho do Socket.IO, como primeiro passo.

<script type="text/javascript" src="/socket.io/socket.io.js"></script>

Note que este caminho apesar de não existir é reconhecido pela biblioteca Socket.IO que incluímos no projeto.

Como segundo passo, devemos modificar o trecho dos eventos click (criado na parte 1 do tutorial), mesclando com o novo objeto socket.

<script>
    var game = new TicTacToe();
    var socket = io.connect();

    //-------------------------------------------------------

    $("#canvas").on("click", function () {
      socket.emit("nova jogada", game.newPlay());
    });

    socket.on("nova jogada", function (params) {
      game.renderSymbol(params.row, params.col);

      if (game.status == "vitoria") {
        socket.emit("vitoria", {
          name: game.currentPlayer.name,
          points: 10
        });
      }

      if (game.status != "empate") {
        game.changePlayer();
      }

    });

    // -------------------------------------------------------

    $("#btn-newgame").on("click", function () {
      socket.emit("novo jogo");
    });

    socket.on("novo jogo", function (params) {
      game.newGame();
    });

    //-------------------------------------------------------

    $("#btn-scoreboard").on("click", function () {
      socket.emit("exibir placar");
    });

    socket.on("exibir placar", function (score) {
      game.showScoreboard(score);
    });

    //-------------------------------------------------------

    $("#btn-reset-scoreboard").on("click", function () {
      socket.emit("zerar placar");
    });

    socket.on("zerar placar", function (score) {
      game.showScoreboard(null);
    });

  </script>

A comunicação entre cliente e servidor ocorrerá por meio de mensagens, então por exemplo, quando, no cliente, emitimos para o servidor uma mensagem novo jogo (no evento click do botão newgame), outra mensagem é emitida pelo servidor para todos os clientes que estão conectados a ele. Os clientes ao receberem essa mensagem realizam alguma ação, como criar um novo jogo, exibir o placar, zerar o placar, etc.

Agora, para que o servidor possa emitir as mensagens de volta para o cliente, temos que alterar o arquivo app.js também. Inclua o código abaixo após a função callback.

// Módulo Socket.IO
var io = require('socket.io')(app);

// Conexão do cliente com o servidor
io.on("connection", function (socket) {

     socket.on("nova jogada", function (params, callback) {
          io.sockets.emit("nova jogada", params);
          if (callback) callback();
     });

     socket.on("novo jogo", function (params, callback) {
          io.sockets.emit("novo jogo", params);
          if (callback) callback();
     });

     socket.on("exibir placar", function (params, callback) {
          io.sockets.emit("exibir placar", params);
          if (callback) callback();
     });

     socket.on("zerar placar", function (params, callback) {
          io.sockets.emit("zerar placar", params);
          if (callback) callback();
     });

     socket.on("vitoria", function (params, callback) {
          if (callback) callback();
     });
});

Quando o usuário acessa a página o método connection é disparado. Depois de efetuar a conexão o objeto socket (representando o cliente conectado) é recebido como parâmetro. Agora o servidor saberá o que emitir para todos os clientes conectados dependendo da mensagem por meio de io.sockets.emit.

Note que passamos a variável app em require(‘socket.io’)(app); para o módulo saber qual é o nosso servidor.

Depois de tudo isso, se você abrir duas janelas do seu navegador e acessar a URL do projeto verá que a mesma ação se repete simultaneamente nas duas.

Persistindo o placar

Até agora nosso placar não mostrou nenhuma pontuação. Poderíamos simplesmente guardar o valor em uma variável, cookie ou cache, mas faremos melhor que isso armazenando no banco, para consultar e atualizar sempre que necessário.

A primeira coisa que precisaremos é de uma maneira de acessar o MongoDB, para isso abra o terminal novamente, acesse o diretório Tutorial e execute o comando “npm install mongodb”. Em seguida crie um novo arquivo chamado db.js neste mesmo diretório, ele será o nosso módulo para interagir com o banco. Nele adicione o seguinte:

var mongoClient = require('mongodb').MongoClient;

var gameDb;

mongoClient.connect('mongodb://localhost:27017/game', { useNewUrlParser: true })
    .then(conn => {
         gameDb = conn.db("game");
    })
    .catch(err => console.log(err));

O que estamos fazendo aqui é incluir o módulo mongodb do mesmo modo que fizemos com os outros e utilizando a classe MongoClient para fazer a conexão.

No método connect passamos como argumentos a URL de conexão e o comando useNewUrlParser para evitarmos DepreciationWarning no console. Na URL temos:

  • mongodb:// protocolo
  • localhost:27017 servidor que estamos conectando
  • /game nome do banco que queremos conectar (não existe ainda, será criado quando inserirmos o primeiro registro)

Em caso de sucesso na conexão guardamos uma referência para nossa base e em caso de erro imprimimos no console a mensagem.

Agora, abaixo insira este outro trecho de código:

// Atualiza a pontuação pelo nome do jogador
function updateScore(name, points, callback) {

    // Procura o documento filtrando pelo nome do jogador
    gameDb.collection('scoreboard').findOne({ name: name }, function (err, player) {

        if (err) {
            console.log(err);
        }
        else {
            // Se o documento foi encontrado atualiza a pontuação, caso contrário, insere um novo documento
            if (player) {
                gameDb.collection('scoreboard').updateOne(
                    { "name": name },
                    { $set: { score: (player.score + points) } },
                    { upsert: true });
            }
            else {
                gameDb.collection('scoreboard').insertOne({ name: name, score: points }, callback);
            }
        }
    });

}

// Retorna a pontuação dos jogadores
function getScore(callback) {
    gameDb.collection('scoreboard').find({}).toArray(callback);
}

// Remove a pontuação dos jogadores
function resetScoreboard(callback) {
    gameDb.collection('scoreboard').deleteMany({}, callback);
}

module.exports = { updateScore, getScore, resetScoreboard };

Aqui temos as funções para atualizar, consultar e remover a pontuação do jogador na collection scoreboard.

Observe os métodos que foram utilizados.

  • findOne: para consultar a coleção passando o nome do jogador como filtro;
  • updateOne: caso o documento seja encontrado atualizamos a pontuação do jogador. Aqui novamente filtrando pelo nome;
  • insertOne: para inserir um novo documento com dois campos, name e score, caso ainda não exista.
  • find: para retornar todos os documentos da coleção e convertê-los em um array de objetos;
  • deleteMany: para apagar tudo que foi inserido na coleção.

Ao final exportamos essas funções com module.exports, isso é necessário porque as funções e variáveis declaradas em um módulo não ficam disponíveis para outros módulos.

Volte ao arquivo app.js. Agora tudo que precisamos fazer é importar o módulo db e modificar alguns dos handlers para chamar as funções que foram exportadas.

Inclua a linha abaixo acima do módulo socket.io…

// Módulo Database
var db = require('./db');

…e modifique estes três handlers

socket.on("exibir placar", function (params, callback) {
          db.getScore((err, result) => {
               if (err) { return console.log(err); }
               io.sockets.emit("exibir placar", result);
               if (callback) callback();
          });
     });

     socket.on("zerar placar", function (params, callback) {
          db.resetScoreboard((err, result) => {
               if (err) { return console.log(err); }
               io.sockets.emit("zerar placar", params);
               if (callback) callback();
          });
     });

     socket.on("vitoria", function (params, callback) {
          db.updateScore(params.name, params.points, (err, result) => {
               if (err) { return console.log(err); }
          });
          if (callback) callback();
     });

Se tudo correu bem temos nosso placar funcionando e acumulando a pontuação dos jogadores. Execute a aplicação para testar.

Aqui vai o link para o projeto no GitHub caso queira fazer download.

Por hoje é só, até o próximo artigo!

Compartilhe!