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

Finalmente chegou a hora de brincar um pouco com o que vimos nos artigos anteriores. Falamos sobre o que são sobre MongoDB e Node.js, como funcionam e quais as vantagens e desvantagens dessas duas tecnologias, porém ainda não vimos uma linha de código sequer.

O que faremos hoje neste tutorial dividido em duas partes é implementar um jogo da velha. A princípio pode parecer algo sem utilidade, mas na prática traz vários conceitos que você poderá aplicar em seus próprios projetos.

Do que vamos precisar?

Para que você consiga acompanhar o tutorial, conhecimentos de HTML, CSS e programação em JavaScript são requisitos básicos.

O sistema que estarei utilizando é o Windows 10, com o navegador Chrome em conjunto com os seguintes programas:

A instalação não é o foco deste artigo, porém não tem segredo, basta fazer o download, clicar no instalador e ir avançando as etapas.

Para saber se o Node está funcionando corretamente abra o terminal (DOS, Shell, bash…) e digite o comando “node –v”, você deverá ver como resultado a versão atual instalada em sua máquina.

No caso do MongoDB se você não especificou o caminho na variável PATH do Windows, digite no terminal o caminho completo onde o seu Mongo foi instalado “C:\Program Files\MongoDB\Server\4.0\bin\mongod.exe” –version.

Mãos à obra!

Ao abrir o Visual Studio Code e temos a tela de boas-vindas.

Clique em Add workspace folder….

Vamos criar um novo workspace para o nosso projeto. Estarei utilizando o caminho C:\Tutorial.

Neste diretório crie dois subdiretórios, um chamado css e outro js para mantermos as coisas organizadas. Em seguida crie um arquivo index.html e outro app.js, também em Tutorial. Ao final você terá algo assim:

Criando um servidor

Node utiliza um sistema de carregamento baseado em módulos, estes módulos nada mais são do que bibliotecas JavasScript que você pode incluir na sua aplicação. Para incluir um módulo basta utilizar a função require(‘NOME_DO_MODULO’).

Criaremos agora nosso servidor HTTP responsável por tratar as requisições.

Abra o arquivo app.js, inclua o código abaixo e salve.

// Módulo HTTP
var http = require('http');

// Cria o objeto server e atribui a variável app
var app = http.createServer(function(req, res){
    res.write('Ola mundo!'); // Escreve a resposta para o cliente
    res.end(); // Fim da resposta
}).listen(3000); // Especifica a porta que vai escutar as requisições

Oparâmetro req contém as informações da requisição e o parâmetro res é o objeto que envia a resposta para o cliente (requisitante).

Agora para testarmos, escolha Debug > Start Debugging (F5). Abra seu navegador e acesse o endereço http://localhost:3000.

Nota: Uma outra maneira de executar a aplicação é acessando o diretório onde está o arquivo app.js pelo terminal e digitando “node app.js”.

O que vemos é uma página com um texto “Ola mundo!”, nada muito empolgante não é mesmo? 🙁

Lendo arquivos

Vamos melhorar um pouco este código incluindo o módulo File System para trabalhar com arquivos de sistema e tratar as requisições que chegam no nosso servidor.

// Módulo HTTP
var http = require('http');

// Módulo File System
var fs = require('fs');

// Cria o objeto server
var app = http.createServer(callback);

app.listen(3000); // Especifica a porta que vai escutar as requisições

function callback(req, res) {

     // Nome do arquivo
     var filename = req.url == "/" ? 'index.html' : __dirname + req.url;

     fs.readFile(filename, function (err, data) {
               if (err) {
                    res.writeHead(404);
                    return res.end('Arquivo não encontrado!');
               }

               if (req.url.indexOf(".css") != -1)
                    res.setHeader('content-type', 'text/css');
               else if (req.url.indexOf(".js") != -1)
                    res.setHeader('content-type', 'text/javascript');
               else
                    res.setHeader('content-type', 'text/html');

               res.writeHead(200);
               res.end(data);
          }
     );
}

Neste novo trecho de código, primeiro carregamos o módulo File System e depois da criação do servidor definimos uma função callback separada agora e nela estamos dizendo que, caso tenhamos somente o caminho da requisição “/”, index.html deve ser retornado, caso contrário, retorne o caminho do diretório do módulo atual (C:\Tutorial) mais o arquivo cujo o nome está sendo passado pela URL.

readFile é o método responsável por ler o arquivo no disco e recebe como parâmetro o caminho do arquivo junto com uma função de retorno, chamada após a leitura.

Para atribuir o tipo do conteúdo do arquivo usamos o método setHeader conferindo sua extensão.

É sério isso?

Eu sei, existem módulos e frameworks criados para fazer todo esse trabalho (organizar e ler os arquivo, definir rotas, atribuir o contente-type, etc.) de forma rápida e sem necessidade de reinventar a roda, como é o caso do ExpressJS, mas para dar mais emoção fins didáticos estamos realizando todo esse processo manualmente, seja paciente jovem padawan chegaremos lá.

Um pouco de HTML

Agora vamos modificar nossa index.html inserindo o seguinte código.

<!DOCTYPE html>
<html>

<head>
  <title>Jogo da Velha</title>
  <link type="text/css" rel='stylesheet' href='/css/style.css' />
</head>

<body>

  <canvas id="canvas"></canvas>
  <div id="result"></div>
  <button id="btn-newgame" class="btn-game">Novo Jogo</button>
  <button id="btn-scoreboard" class="btn-game">Placar</button>
  <button id="btn-reset-scoreboard" class="btn-game">Zerar Placar</button>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script src="/js/game.js"></script>
</body>

</html>

Temos aí um elemento canvas onde iremos renderizar o jogo, logo abaixo uma div para exibir o resultado da partida e três botões, um para iniciar um novo jogo, outro para exibir o placar e outro para limpar o placar, respectivamente.

Repare que fazemos referência a um arquivo css, onde colocaremos os estilos da página, a um arquivo js onde vai ficar a lógica do jogo e a biblioteca jquery para nos auxiliar.

Vamos criar estes dois arquivos, o primeiro, style.css adicione no diretório css e o segundo, game.js, no diretório js. Para adicionar um novo arquivo clique com o botão direito sobre diretório e em New File.

No arquivo css copie e cole o código abaixo.

#canvas {
  cursor: pointer;
  display: block;
  margin: 0 auto;
}

#result {
  color: #fff;
  display: block;
  font-size: 34px;
  margin: 50px auto;
  text-align: center;
  text-transform: uppercase;
  width: 400px;
}

.btn-game {
  background: #2c343e;
  border: 5px solid #2c343e;
  border-radius: 45px;
  color: #fff;
  cursor: pointer;
  display: block;
  font-size: 19px;
  font-weight: 700;
  margin: 13px auto;
  outline: none;
  padding: 11px 30px;
  text-transform: uppercase;
  width: 226px;
}

.btn-game:hover {
  background: #363f4a;
}

a {
  color: #00B7FF;
}

body {
  background: #3d4550;
  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
  padding: 50px;
}

Se você executar a aplicação agora verá esta imagem no seu navegador.

Uma dose de JavaScript

Agora que temos nossa página vamos a lógica do jogo. Para que o artigo não fique mais extenso darei apenas uma visão resumida do código do jogo e de como as coisas se encaixam. Acompanhe lendo os comentários que foram inseridos.

Novamente, copie e cole o código, agora em game.js:

/**
 * Classe TicTacToe
 */
function TicTacToe() {

    // Jogador 1
    this.player1 = { name: "Jogador 1", symbol: "O", color: "#2ce0b7" };

    // Jogador 2
    this.player2 = { name: "Jogador 2", symbol: "X", color: "#33b4d6" };

    // Status do jogo (vitoria ou empate)
    this.status = "";

    // Jogador atual
    this.currentPlayer = this.player1;

    // Largura do canvas
    this.canvasWidth = 400;

    // Altura do canvas
    this.canvasHeight = 400;

    // Contexto de renderização do canvas
    this._ctx = null;

    // Matriz representando o tabuleiro
    this._matrix = null;

    // Indica se a partida está em andamento ou não
    this._isRunning = false;

    // Largura da coluna no tabuleiro
    this._colWidth = 0;

    // Altura da linha no tabuleiro
    this._colHeight = 0;

    // Método de inicialização
    this.init();
};

// Define o valor de algumas propriedades e inicia um novo jogo
TicTacToe.prototype.init = function () {
    var $canvas = $("canvas");

    // Dimensões do canvas
    $canvas[0].width = this.canvasWidth;
    $canvas[0].height = this.canvasHeight;
    $canvas[0].style.width = this.canvasWidth;
    $canvas[0].style.height = this.canvasHeight;

    // Dimensões das colunas (um terço da largura e da altura do canvas, respectivamente)
    this._colWidth = this.canvasWidth / 3;
    this._rowHeight = this.canvasHeight / 3;

    // Estamos dizendo que vamos desenhar imagens 2D
    this._ctx = $canvas[0].getContext("2d");

    this.newGame();
};

// Inicia um novo jogo
TicTacToe.prototype.newGame = function () {

    // Limpa o status
    this.status = "";

    // Limpa o canvas
    this._clearCanvas();

    // Limpa a matriz
    this._matrix = [
        ["", "", ""],
        ["", "", ""],
        ["", "", ""]
    ];

    // Limpa o resultado
    this._setResult('');

    // Cria um novo tabuleiro
    this._createBoard();

    // Altera a flag de jogo em andamento
    this._isRunning = true;
};

// Define uma nova jogada quando o jogador clica em uma região do canvas
TicTacToe.prototype.newPlay = function () {
    if (this._isRunning) {

        // Obtem a coluna e a linha onde o jogador clicou
        var col = Math.floor(event.offsetX / this._colWidth);
        var row = Math.floor(event.offsetY / this._rowHeight);

        // Verifica se a posição na matriz está vazia
        if (this._matrix[row][col] == "") {
            return { row: row, col: col };
        }
    }
    return { row: -1, col: -1 };
};

// Alterna entre um jogador e outro
TicTacToe.prototype.changePlayer = function () {
    if (this.currentPlayer.name == this.player2.name) {
        this.currentPlayer = this.player1;
    }
    else {
        this.currentPlayer = this.player2;
    }
};

// Desenha um símbolo no tabuleiro
TicTacToe.prototype.renderSymbol = function (row, col) {

    if (row != -1 && col != -1) {
        
        // Configura o canvas antes de desenhar o símbolo
        this._ctx.fillStyle = this.currentPlayer.color;
        this._ctx.font = (this.canvasWidth / 5) + "px Arial";
        this._ctx.textAlign = "center";
        this._ctx.textBaseline = "middle";
        this._ctx.fillText(this.currentPlayer.symbol, col * this._colWidth + this._colWidth / 2, row * this._rowHeight + this._rowHeight / 2);
        this._matrix[row][col] = this.currentPlayer.symbol;

        // Verifica se houve vitória ou empate e encerra o jogo
        if (this._isVictory()) {
            this._setResult(this.currentPlayer.name + " venceu!");
            this._isRunning = false;
        }
        else if (this._isTie()) {
            this._setResult("Empate!");
            this._isRunning = false;
        }
    }
};

// Verifica se houve empate
TicTacToe.prototype._isTie = function () {
    var count = 0;
    var cols = this._matrix[0].length;
    var rows = this._matrix.length;

    // Conta a quantidade de posições com algum símbolo e compara com o total de posições
    for (var i = 0; i < cols; i++) {
        for (var j = 0; j < rows; j++) {
            if (this._matrix[i][j] != "") {
                count++;
            }
        }
    }

    var isTie = count == cols * rows;

    if (isTie) {
        this.status = "empate";
    }

    return isTie;
};

// Verifica se houve vitória
TicTacToe.prototype._isVictory = function () {
    if (
        // Compara os símbolos na vertical
        this._compareSymbols(this._matrix[0][0], this._matrix[1][0], this._matrix[2][0]) ||
        this._compareSymbols(this._matrix[0][1], this._matrix[1][1], this._matrix[2][1]) ||
        this._compareSymbols(this._matrix[0][2], this._matrix[1][2], this._matrix[2][2]) ||

        // Compara os símbolos na horizontal
        this._compareSymbols(this._matrix[0][0], this._matrix[0][1], this._matrix[0][2]) ||
        this._compareSymbols(this._matrix[1][0], this._matrix[1][1], this._matrix[1][2]) ||
        this._compareSymbols(this._matrix[2][0], this._matrix[2][1], this._matrix[2][2]) ||

        // Compara os símbolos na diagonal
        this._compareSymbols(this._matrix[0][0], this._matrix[1][1], this._matrix[2][2]) ||
        this._compareSymbols(this._matrix[0][2], this._matrix[1][1], this._matrix[2][0])) {

        // Houve vitória
        this.status = "vitoria";

        return true;
    }
    return false;
};

// Compara três símbolos
TicTacToe.prototype._compareSymbols = function (a, b, c) {
    return a == b && b == c && c != "";
};

// Exibe o resultado
TicTacToe.prototype._setResult = function (text) {
    $("#result").html(text);
};

// Exibe o placar
TicTacToe.prototype.showScoreboard = function (score) {

    this._setResult('');
    this._clearCanvas();
    this._isRunning = false;
    this._ctx.fillStyle = "#ddd";
    this._ctx.font = "30px Arial";
    this._ctx.textAlign = "center";
    this._ctx.textBaseline = "top";

    this._clearCanvas();
    
    // Título
    this._ctx.fillText("Placar", this.canvasWidth / 2, 40);

    if (score == null) {
        this._ctx.fillText("Nenhum placar", this.canvasWidth / 2, this.canvasHeight / 3);
    }
    else {
        if (score.length > 0) {
            for (var i = 0; i < score.length; i++) {
                var player = score[i];
                this._ctx.fillText(player.name + ": " + player.score + " pontos", this.canvasWidth / 2, (i + 1) * 80 + 40);
            }
        }
        else {
            this._ctx.fillText("Nenhum placar", this.canvasWidth / 2, this.canvasHeight / 3);
        }
    }
};

// Limpa o canvas
TicTacToe.prototype._clearCanvas = function () {
    this._ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
};

// Cria o tabuleiro
TicTacToe.prototype._createBoard = function () {
    
    // Cor da linhas
    this._ctx.strokeStyle = "#596575";

    // Espessura das linhas
    this._ctx.lineWidth = 3;

    // Desenha a linha vertical 1
    this._ctx.beginPath();
    this._ctx.moveTo(this._colWidth, 0);
    this._ctx.lineTo(this._colWidth, this.canvasHeight);
    this._ctx.stroke();

    // Desenha a linha vertical 2
    this._ctx.beginPath();
    this._ctx.moveTo(2 * this._colWidth, 0);
    this._ctx.lineTo(2 * this._colWidth, this.canvasHeight);
    this._ctx.stroke();

    // Desenha a linha horizontal 1
    this._ctx.beginPath();
    this._ctx.moveTo(0, this._rowHeight);
    this._ctx.lineTo(this.canvasWidth, this._rowHeight);
    this._ctx.stroke();

    // Desenha a linha horizontal 2
    this._ctx.beginPath();
    this._ctx.moveTo(0, 2 * this._rowHeight);
    this._ctx.lineTo(this.canvasWidth, 2 * this._rowHeight);
    this._ctx.stroke();
};

Essa é uma classe baseada em protótipos chamada TicTacToe e seus métodos servem para:

  • Iniciar um novo jogo;
  • Realizar uma nova jogada, aqui retornamos a linha e a coluna do tabuleiro, onde o jogador clicou;
  • Alternar entre um jogador e outro;
  • Desenhar um símbolo quando um dos dois jogadores clicar no tabuleiro (representado pelo canvas);
  • Verificar se houve empate ou vitória. Para isso examinamos se todas as posições já estão preenchidas ou, no caso de vitória, comparamos os símbolos de três em três analisando cada uma das possibilidade (vertical, horizontal e diagonal);
  • Exibir um texto indicando o resultado da partida;
  • Exibir o placar (a pontuação vai vir do banco, veremos isso mais tarde);
  • Limpar e recriar o tabuleiro desenhando as linhas no canvas.

Para instanciar nossa classe vamos voltar ao código HTML e incluir esta nova tag script abaixo da tag script game.js, que já tínhamos.

<script>
    var game = new TicTacToe();

    $("#canvas").on("click", function () {
      if (game.status == "") {
        var params = game.newPlay();
        game.renderSymbol(params.row, params.col);
        game.changePlayer();
      }
    });

    $("#btn-newgame").on("click", function () {
      game.newGame();
    });

    $("#btn-scoreboard").on("click", function () {
      game.showScoreboard(null);
    });

    $("#btn-reset-scoreboard").on("click", function () {
      game.showScoreboard(null);
    });
</script>

Novamente execute a aplicação e finalmente você poderá jogar! Uau que empolgante….


Enfim, essa foi a primeira parte do tutorial, até agora criamos nosso jogo, mas podemos fazer mais do que isso, veja a continuação no próximo artigo clicando aqui.

Compartilhe!