Jogo da Memória em Vanilla JavaScript
Introdução de conceitos básicos de HTML5, CSS3 e JavaScript puro.
Esse tutorial introduz alguns conceitos básicos de HTML5, CSS3 e Javascript puro (sem jQuery). Vamos discutir sobre o atributo data-*, posicionamento, perspectiva, transições, flexbox, eventos, timeouts e ternários. Não é necessário que você tenha muita experiência com programação para desenvolver o jogo.
- 🕹Demo: Projeto Jogo da Memória
- 📂 Repo: Code Sketch Repo
Estrutura de Arquivos
Vamos começar criando os arquivos no terminal:
🌹 mkdir memory-game
🌹 cd memory-game
🌹 touch index.html styles.css scripts.js
🌹 mkdir img
HTML
Template inicial adicionando os arquivos css
e js
:
O jogo possui doze cartas e cada uma delas consiste em uma div
pai chamada .memory-card
, que possui dois elementos filhos img
. O primeiro, chamado front-face
, representa a face da carta e o segundo, back-face
, o seu verso.
As imagens podem ser baixadas aqui: Repositório
As cartas serão ainda envolvidas por um elemento section
chamado `memory-game’:
CSS
Vamos aplicar um reset simples a todos os elementos:
A propriedade box-sizing: border-box
inclui os valores de padding
e border
no tamanho total do elemento. Assim, ficamos livres da necessidade de subtraí-las ao aplicar largura e altura.
Ao aplicar display: flex no body
e margin: auto
no .memory-game
container, o último será centralizado vertical e horizontalmente.
.memory-game
também será um flex-container
. Por default, os items encolhem a sua largura para se acomodar na largura do container em apenas uma linha. Setando flex-wrap como wrap
, flex-items
ao invés de encolher, retomam a sua largura inicial e ocupam quantas linhas forem necessárias.
A largura e a altura de cada carta é calculada através da função calc(). Para construir três linhas e quatro colunas, aplicamos 25%
para a propriedade width
e 33.333%
para height
, menos os 10px
da margin
.
Para posicionar as divs filhas da .memory-card
absolutamente, adicionamos a propriedade position: relative
, que posiciona o elemento na tela, servindo de referência.
A propriedade position: absolute
adicionada em ambas front-face
e back-face
, remove os elementos da sua posição original e os posiciona um sobre o outro.
O template com as doze cartas deve ficar assim:
Vamos utilizar a pseudo classe :active
, adicionada quando o elemento é clicado, para um efeito de clique. Aplicamos uma transição de .2s
no seu tamanho:
Virar Carta
Para que a carta vire ao ser clicada, a classe flip
será adicionada ao elemento. Selecionamos todos os elementos memory-card
com document.querySelectorAll
, iteramos através da lista com forEach
e adicionamos o detector de evento com addEventListener
. Toda vez que uma carta for clicada a função flipCard
será chamada. A variável this
representa a carta que foi clicada. A função acessa a lista de classes do elemento (classList
), se a classe flip
não estiver na lista, ela é adicionada e se estiver, é retirada:
No CSS a classe flip
rotaciona a carta 180°:
Para produzir o efeito 3D, vamos adicionar a propriedade perspective ao container .memory-game
. Essa propriedade é responsável pelo valor da distância entre o usuário e o elemento ao longo do plano z
. Quanto menor o valor, maior é o efeito de perspectiva. Para um efeito sutil vamos aplicar 1000px
.
Para posicionar o elemento .memory-card
no espaço 3D criado no container, aplicamos a propriedade transform-style: preserve-3d
, caso contrário a carta continuará achatada no plano z = 0
(transform-style).
Para gerar o efeito de movimento, aplicamos uma transição para a propriedade transform
:
O efeito 3D funciona 🎉! Mas por que a face da carta não está aparecendo? Nesse momento, .front-face
e .back-face
estão sobrepostas, deviso à propriedade position: absolute
. Todo elemento possui um verso
, que é a imagem espelhada da sua face
. A propriedade backface-visibility tem como valor default visible
, assim, quando viramos a carta, o que vemos é o verso do logo do Javascript.
Para que possamos enxergar a face da carta, aplicammos backface-visibility: hidden
em ambas .front-face
e .back-face
:
Se atualizarmos a página e viramos a carta, ela some! 😯
Como nós escondemos o verso de ambas as imagens, não há nada do outro lado. Então precisamos virar a .front-face
180°:
E agora sim, conseguimos o efeito!
Lógica do Jogo
Quando clicamos na primeira carta, precisamos esperar a segunda carta virar. As variáveis hasFlippedCard
e flippedCard
se encarregarão de gerenciar o estado do jogo. Ao primeiro clique, se não houver carta virada, hasFlippedCard
é setada como true
e flippedCard
guarda a carta clicada. Vamos trocar o método toggle
para add
:
Quando a segunda carta for clicada, entraremos no bloco else
. Checaremos então se as cartas são iguais. Mas para isso, precisamos identificar as cartas.
Para adicionar informação extra em elementos HTML, podemos utilizar o atributo data-*. A partir da seguinte sintaxe: data-*
, onde *
pode ser qualquer palavra, o atributo será inserido na propriedade dataset
do elemento. Vamos então adicionar data-framework
em cada carta:
Agora podemos testar se as cartas formam pares acessando o seu dataset
. Vamos extrair a lógica para o método checkForMatch()
e setar hasFlippedCard
para false
. Se as cartas formarem um par, disableCards()
é chamada e os detectores de eventos são removidos, para prevenir que as cartas sejam viradas novamente. Caso contrário, unflipCards()
remove a classe .flip
após 1500ms
e a carta retorna a sua posição inicial.
Juntando tudo:
Uma forma mais elegante de escrever o método checkForMatch()
pode ser alcançada usando um ternário. Composto por três blocos: o primeiro é a condição a ser avaliada. O segundo é a expressão a ser executada caso a condição seja verdadeira, e o terceiro, caso seja falsa.
Bloqueio do Tabuleiro
Já cobrimos o pareamento das cartas, agora precisamos travar as cartas para evitar que mais de um par de cartas seja virado ao mesmo tempo, senão as cartas não serão desviradas corretamente.
Vamos declarar uma variável lockBoard
. Quando o jogador clicar na segunda carta, setamos lockBoard
como true
e a condição if (lockBoard) return;
previne que qualquer outra carta seja virada até que as cartas desvirem.
Duplo Clique
Ainda devemos tratar o caso que o jogador clica duas vezes sobre a mesma carta. A condição de pareamento seria avaliada como verdadeira, removendo o detector de evento da carta incorretamente.
Para prevenir esse comportamento, vamos avaliar se a segunda carta clicada é a mesma que a primeira e retornar em caso positivo:
As variáveis firstCard
e secondCard
precisam ser resetadas após cada rodada. Vamos criar um método resetBoard()
e extrair hasFlippedCard = false;
e lockBoard = false
para lá. Vamos utilizar o destructuring assignment do ES6: [var1, var2] = ['value1', 'value2']
, para enxugar o código:
O novo método será chamado por ambas as funções disableCards()
e unflipCards()
:
Embaralhar
O jogo está quase pronto, só falta embaralhar as cartas.
Quando display: flex
é declarada no container, flex-items
são ordenados a partir da seguinte hierarquia: ordem de grupo e de código fonte. Cada grupo é definido pela propriedade order, que possui como valor um número inteiro, positivo ou negativo. Seu valor default é 0
, o que significa que todos os elementos pertencem ao mesmo grupo e serão ordenados pela ordem em que aparecem no código fonte. Se existir mais de um grupo, os elementos são primeiro ordenados ascendentemente por grupo.
Vamos iterar através das doze cartas do tabuleiro, gerar um número aleatório entre 0 e 11 e atribuí-lo à propriedade order
:
Para invocar a função shuffle
, vamos transformá-la em uma Immediately Invoked Function Expression (IIFE), assim ela será executada logo após a sua definição. A versão final do script é mostrada abaixo:
And that’s all folks!
References
- Marina Ferreira — Flexbox Fundamentals
- MDN Web Docs — Main Axis
- MDN Web Docs — Cross Axis
- MDN Web Docs — calc
- MDN Web Docs — perspective
- MDN Web Docs — transform-style
- MDN Web Docs — backface-visibility
- MDN Web Docs — Using data attributes
- MDN Web Docs — order
- MDN Web Docs — IIFE
- MDN Web Docs — ternary operator
- MDN Web Docs — destructuring assignment
Originally published at marina-ferreira.github.io.