Praticando HTML, CSS e Javascript Vanilla - Reproduzindo o Jogo da Vida de John Conway

akadot_

Murilo Oliveira

Posted on December 2, 2021

Praticando HTML, CSS e Javascript Vanilla - Reproduzindo o Jogo da Vida de John Conway

English version here.

Introdução:

E ae, se você está procurando projetos legais para praticar seus conhecimentos em Javascript, CSS e HTML, neste tutorial eu vou te ensinar (ou tentar) como reproduzir sua própria versão do Jogo da Vida, do matemático britânico John Conway.

Caso você nunca tenha ouvido falar desse jogo, ele faz parte de uma categoria chamada “autómatos celulares”, que de acordo com a nossa querida Wikipédia: “são os modelos de evolução temporal mais simples com capacidade para exibir comportamento complicado”.

Mas não se preocupe com essas definições complicadas, basicamente o que vamos construir é um jogo que não precisa de jogador, que “se joga sozinho”, quase como se você criasse algo vivo e observasse sua evolução (daí o nome).

O resultado final consiste em um campo preenchido aleatoriamente por quadrados iguais que, ao decorrer do tempo e seguindo algumas regras de sobrevivência, podem gerar estruturas fascinantes e imprevisíveis, como a figura abaixo.

Exemplo de Estrutura do Jogo da Vida

Ok, sem mais enrolação, bora fazer essa bagaça. Deixarei abaixo o link do meu repositório com o projeto finalizado:

https://github.com/akadot/game-of-life

Construção:

Para construir esse projeto, usaremos um recurso bastante poderoso do HTML chamado Canvas API, que permite desenhar formas 2D e até 3D utilizando somente Javascript puro. Mas não se assuste, não precisaremos configurar nada e nem elaborar modelos matemáticos de álgebra linear complexos, o Canvas API é uma simples tag nativa do HTML e esse projeto vai depender puramente de lógica.

O primeiro passo é preparar os três arquivos que iremos utilizar, começando pela estrutura do HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="style.css" />
    <title>Jogin da Vida</title>
  </head>
  <body>
    <canvas id="board"></canvas>

    <script src="game.js"></script>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Como pode ver, usaremos apenas uma tag <canvas> com um id de referência para o JS.

Em seguida, podemos preparar o arquivo CSS, aqui vai da criatividade de cada um:

* {
  padding: 0;
  margin: 0;
  outline: 0;
  box-sizing: border-box;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #000000;
}

#board {
  border: 5px solid #5c3ec9;
  border-radius: 5px;
  background-color: #f8f8f2;
  box-shadow: 0px 0px 10px #5c3ec9;
}

Enter fullscreen mode Exit fullscreen mode

Pronto, agora é só abrir o arquivo HTML em seu navegador e...

Canvas Vazio

Eu sei, parece só um quadrado comum, mas eu prometo que vao se tornar algo legal. Para isso devemos começar a definir as propriedades e funcionalidades desse Canvas.
Existem várias maneiras de definir as propriedades de um Canva, porém optaremos por fazer tudo no nosso arquivo Javascript. É o que iremos fazer a seguir.

Implementando a Lógica:

Hora de construir de fato as coisas. O primeiro passo é referenciar a tag <canva> no nosso arquivo de Javascript e, logo depois, informar se queremos trabalhar com o canva 2D ou 3D (no nosso caso é o 2D):

const canvas = document.querySelector("#board");
const ctx = canvas.getContext("2d"); 
//ctx define o contexto do nosso canvas, no caso será 2D
Enter fullscreen mode Exit fullscreen mode

Em seguida, definiremos algumas constantes que nos ajudarão no decorrer do código, como altura, largura e a resolução dos blocos:

const GRID_WIDTH = 500;             //largura do campo
const GRID_HEIGHT = 500;           //altura do campo
const RES = 5;                    //tamanho dos lados dos quadrados
const COL = GRID_WIDTH / RES;    //quantidade de colunas
const ROW = GRID_HEIGHT / RES;  //quantidade de linhas
Enter fullscreen mode Exit fullscreen mode

Feito isso, vamos utilizar essas constantes para definir as informações do canvas:

canvas.width = GRID_WIDTH;
canvas.height = GRID_HEIGHT;
Enter fullscreen mode Exit fullscreen mode

Pronto, agora eu prometo que vamos conseguir visualizar algo no navegador, mas para ter certeza que tudo carregará corretamente, colocarei nosso código dentro de um evento, que só irá disparar quando todo o HTML estiver carregado. Assim não precisaremos nos preocupar caso o código inicie antes das criação do canvas:

document.addEventListener("DOMContentLoaded", () => {
  const canvas = document.querySelector("#board");
  const ctx = canvas.getContext("2d");

  const GRID_WIDTH = 500;
  const GRID_HEIGHT = 500;
  const RES = 5;
  const COL = GRID_WIDTH / RES;
  const ROW = GRID_HEIGHT / RES;

  canvas.width = GRID_WIDTH;
  canvas.height = GRID_HEIGHT;
}
Enter fullscreen mode Exit fullscreen mode

O próximo passo é desenhar nossos blocos dentro do nosso campo. Para isso, criaremos um array, com duas dimensões, que irá armazenar a mesma quantidade de linhas e colunas do nosso canvas, além do tamanho dos nossos blocos.

Para isso, definiremos uma função chamada createGrid(), que vai receber o número de linhas (ROW) e colunas (COL). Em seguida retornaremos um novo array com o mesmo tamanho do número de colunas e, para cada item/coluna nesse array, criaremos um novo array do mesmo tamanho das nossas linhas. Pode parecer complexo mas é bem simples na prática, aproveitaremos também para preencher todos os valores desses arrays com zero (0) e um (1) de forma aleatória, mas explicarei isso mais tarde:

function createGrid(cols, rows) {
    return new Array(cols)
      .fill(null)
      .map(() => new Array(rows)
                 .fill(null)
                 .map(() => Math.round(Math.random())));
}

let grid = createGrid(COL, ROW); //por fim, executamos a função e armazenamos os arrays em uma variável grid
Enter fullscreen mode Exit fullscreen mode

Com nosso array criado, podemos começar a desenhar os blocos na tela, baseado no valor de cada célula dentro do array, onde os valores 1 serão pintados e os valores 0 ficarão apagados.

Para isso, precisaremos de uma função que percorra todo o array e desenhe no canvas seguindo essas regras. Criaremos então a função drawGrid(), que receberá nosso grid, as linhas e colunas do nosso canvas e a resolução/dimensão dos nossos blocos:

function drawGrid(grid, cols, rows, reslution) {
    ctx.clearRect(0, 0, cols, rows);
    for (let i = 0; i < cols; i++) {
      for (let j = 0; j < rows; j++) {
        const cell = grid[i][j];
        ctx.fillStyle = cell ? "#5c3ec9" : "#f8f8f2";
        ctx.fillRect(i * reslution, j * reslution, reslution, reslution);
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Como podem ver, primeiramente executaremos a função clearRect() nativa do Canvas API, que tem o papel de limpar o canvas antes de começarmos a desenhar. Ela recebe nos dois primeiros parâmetros, as coordenadas de onde deverá começar a limpar, já nos dois últimos, onde deve terminar sua limpeza.

Feito isso, criamos dois laços de repetição for que percorrerá as linhas e colunas do nosso array (eu sei que dava pra fazer algo melhor ou usar a função *.map(), mas ficaremos no for por enquanto)*. Dentro do laço, adicionaremos a célula atual em uma constante **cell, para logo em seguida verificar se ela possui um 0 ou um 1 usando um if ternário.

Nessa linha usamos outra propriedade nativa do Canvas API, o fillStyle, que recebe a cor que usaremos para pintar nossos blocos. No nosso caso, ele pode receber a cor #5c3ec9 caso a célula possua um valor 1 (que no javascript significa verdadeiro/true) ou um valor 0 (que no javascript significa vazio/falso/inexistente).

Na linha abaixo, mai uma tag nativa, mas dessa vez é a função fillRect(), que irá desenhar de fato nosso bloco, que será um simples retângulo. Essa função precisa de 4 parâmetros:

  • Primeiro: a coordenada X de onde o retângulo deverá ser iniciado (no nosso caso será a resolução x a posição no array);
  • Segundo: a coordenada Y de onde o retângulo será iniciado (no nosso caso será a resolução x a posição no array);
  • Terceiro: a largura do retângulo (a resolução);
  • Quarto: a altura do retângulo (a resolução);

Feito isso, agora você pode desenhar os quadrados dentro do canvas, executando a função que acabamos de criar desse jeito:

drawGrid(grid, COL, ROW, RES);
Enter fullscreen mode Exit fullscreen mode

Canvas Desenhado com os Retângulos Aleatórios

Explicando as Regras do Jogo

Antes de prosseguirmos, precisamos entender as regras propostas por John Conway, para que o jogo seja de fato "auto-jogável".

Felizmente, existem apenas 4 regras bem simples, tanto de entender quanto de implementar, que definem se uma célula está "viva", que no nosso caso serão as células roxas, ou "morta", que aqui serão as células vazias. Para isso, as condições propostas são:

  • 1: Qualquer célula viva com menos de dois vizinhos vivos morre de solidão;
  • 2: Qualquer célula viva com mais de três vizinhos vivos morre de superpopulação;
  • 3: Qualquer célula morta com exatamente três vizinhos vivos se torna uma célula viva;
  • 4: Qualquer célula viva com dois ou três vizinhos vivos continua no mesmo estado para a próxima geração.

Regras do Jogo da Vida de John Conway

Seguindo essas regras, vamos desenvolver uma função que aplique todas essas condições ao passar do tempo que o jogo está executando. Ela terá o papel de percorrer todas as células do array, aplicar as condições de vida ou morte e gerar um novo array que será novamente desenhado na tela com a função drawGrid().

A cada repetição desse ciclo, consideraremos que o array gerado é uma nova geração de indivíduos que herdaram as condições da geração anterior. Agora vamos começar a implementar essa função.

Chamaremos a função responsável pela aplicação das regras de nexGen() e, como primeiro passo para não afetarmos o grid anterior, definiremos uma constante capaz de armazenar uma cópia da geração anterior a ela.

function nextGen(grid) {
    const nextGen = grid.map((arr) => [...arr]);
Enter fullscreen mode Exit fullscreen mode

Caso ainda não conheça, no trecho [...arr] utilizamos o operador SPREAD, que foi adicionado ao Javascript a partir da versão 6 e tem como finalidade, armazenar um maior número de informações de uma vez só, muito utilizado com arrays e objetos. Você também pode utilizar as funções .push() ou .slice() ao invés do operador spread, não há problema algum nisso.

O próximo passo é começar os loops que irão percorrer o array para aplicar as regras do jogo. Como fizemos lá em cima, precisamos percorrer todas as linhas, utilizando o grid.length e depois todas as colunas, utilizando o grid[col].length (o parâmetro col é apenas o nome que eu dei a variável de controle do for, mas você pode usar as letras i e j como normalmente é feito).

Já aproveitaremos para capturar a célula inicial em uma constante e criaremos uma variável para contabilizar o número de células vizinha vivas.

 for (let col = 0; col < grid.length; col++) {
      for (let row = 0; row < grid[col].length; row++) {
        const currentCell = grid[col][row];
        let sumNeighbors = 0;
Enter fullscreen mode Exit fullscreen mode

A próxima etapa é, para cada célula, percorrer todos os seus 8 vizinhos e verificar se eles estão vivos ou não. Pode parecer um pouco difícil de entender o código a primeira vista, mas aqui vai uma explicação com imagens:

Ilustrando as Células Vizinhas

Sim, eu utilizei o Google Sheets para isso, mas o importante é que nosso próximo loop vai percorrer os valores entre -1 e 1, localizando o número de vizinhos vivos.

for (let i = -1; i < 2; i++) {
 for (let j = -1; j < 2; j++) {
   if (i === 0 && j === 0) {
     continue; 
   }
Enter fullscreen mode Exit fullscreen mode

Colocamos a condição if (i === 0 && j === 0), pois essa é a posição da célula atual, que não queremos somar no número de vizinhos.

O próximo trecho vai tratar dos "cantos" do nosso campo. Pense assim, se uma célula está colada na lateral esquerda do nosso canvas, não teremos como acessar os vizinhos que estão em uma coluna anterior a ela, ou seja, mais a esquerda, pois eles não existem. Sendo assim, vamos somar valores à variável sumNeighbors somente se suas coordenadas estiverem dentro dos limites do canvas.

const x = col + i
const y = row + j;

if (x >= 0 && y >= 0 && x < COL && y < ROW) {
    const currentNeighbor = grid[col + i][row + j];
    sumNeighbors += currentNeighbor;
Enter fullscreen mode Exit fullscreen mode

Satisfeitas as condições, a variável sumNeighbors receberá seu valor anterior, mais o valor das células vivas, lembrando que as células mortas aqui recebem o valor zero, que não impacta na soma.

Feito isso, podemos aplicar as regras descritas pelo John Conway com um simples if/else:

if (currentCell === 0 && sumNeighbors === 3) {
   nextGen[col][row] = 1;
} else if (currentCell === 1 && (sumNeighbors < 2 || sumNeighbors > 3)){
    nextGen[col][row] = 0;
}
Enter fullscreen mode Exit fullscreen mode

Explicando, a primeira condição testa se a célula atual está vazia e se ela possui 3 vizinhos, se for verdadeiro a próxima geração receberá nessa mesma posição o valor 1 ou vivo.

Já a segunda condição reúne as demais regras em uma só, testando se a célula atual está viva e; se existirem menos de dois vizinhos próxima geração receberá zero, se existirem mais do que 3 vizinhos a próxima geração também receberá zero.

Por fim basta retornar a próxima geração return nextGen;, e a função ficará assim:


  function nextGen(grid) {
    const nextGen = grid.map((arr) => [...arr]); //make a copy of grid with spread operator

    for (let col = 0; col < grid.length; col++) {
      for (let row = 0; row < grid[col].length; row++) {
        const currentCell = grid[col][row];
        let sumNeighbors = 0; //to verify the total of neighbors

        //Verifying the 8 neigbours of current cell
        for (let i = -1; i < 2; i++) {
          for (let j = -1; j < 2; j++) {
            if (i === 0 && j === 0) {
              continue; // because this is the current cell's position
            }

            const x = col + i;
            const y = row + j;

            if (x >= 0 && y >= 0 && x < COL && y < ROW) {
              const currentNeighbor = grid[col + i][row + j];
              sumNeighbors += currentNeighbor;
            }
          }
        }

        //Aplying rules
        if (currentCell === 0 && sumNeighbors === 3) {
          nextGen[col][row] = 1;
        } else if (
          currentCell === 1 &&
          (sumNeighbors < 2 || sumNeighbors > 3)
        ) {
          nextGen[col][row] = 0;
        }
      }
    }
    return nextGen;
  }
Enter fullscreen mode Exit fullscreen mode

Fazendo isso, estamos quase perto de finalizar nosso projeto, o próximo passo é bem simples, criaremos uma função chamada update() para executar todas as funções criadas em ordem, e utilizaremos a função requestAnimationFrame(), nativa do Javascript, para repetir o processo em loop no navegador.

requestAnimationFrame(update);

function update() {
  grid = nextGen(grid);
  drawGrid(grid, COL, ROW, RES);
  requestAnimationFrame(update); //executando novamente para que o loop não pare
}
Enter fullscreen mode Exit fullscreen mode

Pronto, agora está tudo pronto e seu arquivo deve ter ficado assim:

document.addEventListener("DOMContentLoaded", () => {
  const canvas = document.querySelector("#board");
  const ctx = canvas.getContext("2d");

  const GRID_WIDTH = 500;
  const GRID_HEIGHT = 500;
  const RES = 5;
  const COL = GRID_WIDTH / RES;
  const ROW = GRID_HEIGHT / RES;

  canvas.width = GRID_WIDTH;
  canvas.height = GRID_HEIGHT;

  //Making a grid and filling with 0 or 1
  function createGrid(cols, rows) {
    return new Array(cols)
      .fill(null)
      .map(() =>
        new Array(rows).fill(null).map(() => Math.round(Math.random()))
      );
  }

  let grid = createGrid(COL, ROW);

  requestAnimationFrame(update);
  function update() {
    grid = nextGen(grid);
    drawGrid(grid, COL, ROW, RES);
    requestAnimationFrame(update);
  }

  //Generate nex generation
  function nextGen(grid) {
    const nextGen = grid.map((arr) => [...arr]); //make a copy of grid with spread operator

    for (let col = 0; col < grid.length; col++) {
      for (let row = 0; row < grid[col].length; row++) {
        const currentCell = grid[col][row];
        let sumNeighbors = 0; //to verify the total of neighbors

        //Verifying the 8 neigbours of current cell
        for (let i = -1; i < 2; i++) {
          for (let j = -1; j < 2; j++) {
            if (i === 0 && j === 0) {
              continue; // because this is the current cell's position
            }

            const x = col + i;
            const y = row + j;

            if (x >= 0 && y >= 0 && x < COL && y < ROW) {
              const currentNeighbor = grid[col + i][row + j];
              sumNeighbors += currentNeighbor;
            }
          }
        }

        //Aplying rules
        if (currentCell === 0 && sumNeighbors === 3) {
          nextGen[col][row] = 1;
        } else if (
          currentCell === 1 &&
          (sumNeighbors < 2 || sumNeighbors > 3)
        ) {
          nextGen[col][row] = 0;
        }
      }
    }
    return nextGen;
  }

  //Draw cells on canvas
  function drawGrid(grid, cols, rows, reslution) {
    ctx.clearRect(0, 0, cols, rows);
    for (let i = 0; i < cols; i++) {
      for (let j = 0; j < rows; j++) {
        const cell = grid[i][j];
        ctx.fillStyle = cell ? "#5c3ec9" : "#f8f8f2";
        ctx.fillRect(i * reslution, j * reslution, reslution, reslution);
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Agora basta executar o arquivo HTML para vermos isso (ou algo melhor no seu caso, pois tive alguns problemas para gravar minha tela):

Demonstração Final do Projeto

Considerações Finais

Apesar de não parecer grande coisa, esse projeto é muito interessante para treinar os conhecimentos básicos de HTML, CSS e JS, principalmente na manipulação de arrays. Caso tenha se interessado, vou deixar alguns links de projetos maiores que utilizaram os mesmos conceitos desse jogo.

Criando o Jogo da Vida em Excel - https://github.com/asgunzi/JogodaVidaExcel

O Vídeo que me Inspirou, do canal O Programador - https://youtu.be/qTwqL69PK_Y

Espero que tenha gostado e que tenha conseguido aprender algo legal, lembre-se sempre do que Bob Ross dizia: "contanto que esteja aprendendo, você não está falhando".

Apenas continue, mesmo que devagar.

Até mais. ✌️

💖 💪 🙅 🚩
akadot_
Murilo Oliveira

Posted on December 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related