[Tutorial] Fazendo um jogo de memória em JavaScript (parte básica)
28 mai
Olá pessoas
Eu queria ter postado esse tutorial já na semana passada, mas de lá para cá eu tive alguns problemas, e provas e, bom, acabei não programando nesses últimos dias. Em todo caso, esse tutorial vai explicar como fazer o joguinho básico que se encontra aqui, que eu havia mencionado no post anterior. Eu quero fazer algumas adições a ele, colocar mais algumas funcionalidades mas, em todo caso, eu achei que seria legal fazer o tutorial para o básico e depois fazer outros pequenos tutoriais sobre as outras funcionalidades.
Esse tutorial não requer que você saiba muito de JavaScript e/ou jQuery, mas ter alguma noção ajuda, e já ter programado também. Não é muita coisa para explicar, então eu vou tentar explicar tudo com detalhes para ajudar quem também está aprendendo JavaScript e/ou jQuery, OK?
De qualquer maneira, eu ainda estou aprendendo a escrever tutoriais, embora já tenha escrito alguns, então eu sempre posso esquecer de falar algo importante… se for o caso, pode perguntar nos comentários que eu respondo e, se possível, melhoro o tutorial, ok?
O Esqueleto da Aplicação: HTML e CSS
Como não é feito nada de muito avançado nem no HTML nem no CSS, vamos apenas dar uma olhada rápida na estrutura dos dois, ok?
| Clique em 'mostrar' para ver o HTML | |
|---|---|
Como vocês podem ver, a parte HTML desse joguinho é realmente BEM simples. Só quero ressaltar alguns detalhes:
- São inseridos dois scripts: o script do jQuery, e o script do jogo. A ordem é importante, já que eles são carregados em ordem – se o jogo.js estivesse sendo carregado primeiro ele daria erro.
- Estão vendo esses “div” no corpo da página? Sem entrar em aspectos técnicos, eu gosto de imaginá-los como marcações, um quadradinho colorido. Mais tarde eu vou mexer nesses “div” via CSS e via javascript e vou chamar o elemento cujo ID é mesa-de-jogo e o div certo vai ficar levantando os braços dizendo “me usa, me usa, sou eu, sou eu!” (imaginação fértil: eu tenho).
- A parte de tentativas não está na página atual, apenas no arquivo do meu PC, mas é algo bem simples. Se você quiser implementar as tentativas, use esse div, senão, não use.
O CSS tem mais alguns detalhes, mas também é bastante simples:
| Clique em 'mostrar' para ver o código CSS | |
|---|---|
Novamente, nada de muito impressionante, apenas alguns detalhes para deixar a página mais “bonitinha”. Olhando em cada elemento, nós podemos ver:
- body é a página em si, foi definido só para colocar uma cor de fundo
- h1 é o título, definido aqui apenas para definir cor, alinhamento e fonte (ou melhor, a família da fonte mas, enfim)
- Esse ‘.carta img:hover’ dita o comportamento de quando o mouse está sobre (hover) uma imagem (elemento img) que esteja aninhado em um elemento da classe ‘carta’. Ou seja, isso será aplicado apenas a imagens desta categoria. Neste caso não faz muita diferença, já que as únicas imagens da página são aquelas do jogo, mas vale pela organização. Esse estilo serve para mudar o cursos do mouse para “pointer”, para indicar que é possível interagir com aquelas cartas.
- Nesse caso, em “.carta img” estamos definindo um estilo para as cartas, nesse caso, uma margem de 5 pixels para elas não ficarem “grudadas” umas nas outras.
- As duas linhas definindo o estilo de carta e mesa-de-jogo servem para que as cartas fiquem juntas… se você não definir nada, as cartas vão ficar uma em cada linha, o que não ficaria muito bonito :p Inicialmente eu havia usado tabelas para organizar isso, mas assim o código fica mais limpo (a parte de Javascript não tem de se preocupar com essa parte de layout, o que é importante). Ou seja, a mesa de jogo mostra seus elementos “filhos” em um bloco, e as cartas que estão dentro da mesa de jogo estão configuradas para serem mostradas “inline”, ou seja, serem colocadas na mesma linha e não em uma nova linha, como seria padrão em um div.
- Bom, o text-tentativas e o tentativas não tem nada de diferente e, novamente, só são usados se você quiser fazer uma linha sob as cartas com o número de tentativas do jogo.
Uma nota sobre # e . : essas são dois marcados, usados tanto no CSS quando para manipular DOM. # serve para ID e . para classe. Não é difícil de imaginar que ID é um identificador único de um elemento na página, não é mesmo? E classe, por outro lado, pode ter vários elementos em uma página. É o caso de #mesa-de-jogo e .carta: só existe uma mesa de jogo, mas varias cartas, daí se usar mesa-de-jogo como ID e carta como classe.
A diferença pode não parecer relevante, mas existem diversas funções que encontram um elemento com um ID, por exemplo, como o document.getElementById(), que vai retornar um único elemento.
E isso encerra a parte de HTML e CSS. Agora, vamos para a parte divertida: Javascript e jQuery!
Javascript e jQuery: montando a lógica do jogo
Então, nós temos um esqueleto em HTML e CSS, mas do jogo nós ainda não temos absolutamente nada, não é mesmo? É aí que entra a parte de JavaScript e jQuery. O código é bastante simples e eu fiquei em dúvida de por onde começar a explicar… até ter a brilhante idéia de começar explicando… por onde eu comecei
(eu tenho umas sacadas geniais, eu sei)
Então, a primeira coisa que eu fiz foi mostrar as cartas na tela ao abrir a página:
$(function (){
mesa_de_jogo.linhas = 2;
mesa_de_jogo.colunas = 4;
for (linha=1;linha<=mesa_de_jogo.linhas;linha++)
{
for (coluna=1;coluna<=mesa_de_jogo.colunas;coluna++)
{
$("<div class='carta'><img></img></div>").bind("click",function(){ mudarCarta(this);}).appendTo("#mesa-de-jogo");
}
$("#mesa-de-jogo").append("<br>");
}
novoJogo();
});
Por onde começar? Bom, essa função de jQuery, simplesmente $( function () { coisas } ) é carregado assim que o DOM é carregado. Ou seja, esse é, essencialmente, o nosso Main().
Serão colocadas 8 cartas, em duas linhas e quatro colunas, como pode ser visto em mesa_de_jogo.linhas e mesa_de_jogo.colunas. mesa_de_jogo é um objeto que eu criei, apenas para guardar esses dois valores de maneira significativa. Na primeira versão eu usei simplesmente as variáveis linhas e colunas e não faz diferença.
O primeiro for percorre as linhas, e o segundo for percorre as colunas, colocando cada carta na página. O código responsável por adicionar a carta à página é este:
$("<div class='carta'><img></img></div>").bind("click",function(){ mudarCarta(this);}).appendTo("#mesa-de-jogo");
A cifra é o “seletor”, a grande ferramente de jQuery: permite que eu faça inúmeras coisas com os objetos do DOM de maneira muito mais prática que usando Javascript puro.
DOM?
Eu suponho que muitos aqui já saibam o que é DOM, mas vamos a uma explicação simples para aqueles que não sabem: trata-se de uma “árvore” de elementos. Uma página HTML é montada como se fosse uma árvore, cujo tronco principal é o “html”, os dois primeiros grandes galhos são “head” e “body” e daí vão se acrescentando mais galhos e folhas a esta árvore. Por exemplo, a árvore deste nosso jogo poderia ser representada assim (só coloquei os ramos referentes à parte de carta e mesa de jogo, para simplificar):

Eu até tentei desenhar como uma árvore, para ficar mais fácil de visualizar
Mexer com HTML/CSS/JS basicamente envolve mexer bastante com essa árvore, mexendo nos ramos, adicionando folhas, etc. No caso, com
$("<div class='carta'><img></img></div>")
Nós criamos um ramo, com o elemento carta, e que possui uma folha chamada img. Ao colocar isso dentro de $() nós podemos realizar operações sobre esse ramo mais facilmente.
Por exemplo, após a linha acima nós temos a seguinte função:
.bind("click",function(){ mudarCarta(this);})
O que isso faz? Basicamente faz uma conexão com o ramo. Bind funciona com .bind(evento, função). Neste caso, estamos dizendo que ao ocorrer o evento “click” sobre o ramo supracitado deve ser chamada a função mudarCarta(). “this” é passado como parâmetro da função, para que a função saiba qual carta foi clicada.
(Veremos essa função mudarCarta() mais tarde, calma, mas já é possível imaginar o que ela faz, não?).
Ou seja, já temos um ramo “carta”, que possui um evento de clique atrelado a ele. Porém, este ramo AINDA não está na árvore do documento. Ela ainda precisa ser anexada a algum elemento já existente da árvore do documento. No caso, nós queremos anexar esse ramo ao ramo “mesa-de-jogo. e a função .appendTo(elemento) faz isso. Agora, é possível entender totalmente o que faz a linha principal dessa parte do programa, não?
$("<div class='carta'><img></img></div>").bind("click",function(){ mudarCarta(this);}).appendTo("#mesa-de-jogo");
Feito isso você irá perceber que por enquanto são apenas elementos… embora exista o elemento img, ele não tem o atributo “src”, ou seja, ele não está ligado a nenhuma imagem. Isso e outras inicializações são feitas na função novoJogo(), que é chamada depois que os dois laços for são executados e todas as cartas já estão na árvore do documento:
function novoJogo(){
cartaLadoB.sort(function() {return 0.5 - Math.random()});
$(".carta img").attr("src","images/carta.png");
pares_restantes = pares;
tentativas = 0;
$("#tentativas").text(tentativas);
}
O que essa função faz? Basicamente, quatro coisas:
- Embaralha as cartas
- Define a imagem do fundo da carta
- Diz quantos pares faltam para terminar o jogo
- Reseta o número de tentativas
Embaralhando as Cartas
Essa parte de embaralhar as cartas é feita com uma única linha:
cartaLadoB.sort(function() {return 0.5 - Math.random()});
cartaLadoB é um array que eu define com o nome das imagens dos pares. No caso, no início do documento javascript eu a declaro da seguinte forma:
var cartaLadoB = ["bear2-card.png","bear-card.png","leaf-card.png","leaf2-card.png", "bear2-card.png","bear-card.png","leaf-card.png","leaf2-card.png"];
A função sort é a responsável por “embaralhar” esse array. Se eu simplesmente chamada cartaLadoB.sort() esse array seria ordenado em ordem alfabética, mas .sort() é uma função de JavaScript MUITO BACANINHA que te permite dizer COMO você quer organizar o array.
Basicamente, você passa uma função que pode retornar < 0, 0 ou > 0, e o sort organiza os elementos de acordo. Se você comparar os elementos a e b e retornar < 0, a função irá definir que a deve vir antes de b, se > 0, b deve vir antes de a, e se for 0 eles são considerados iguais e suas posições não mudam. Sabendo disso, é possível fazer uma infinidade de funções para fazer os mais diferentes tipos de ordenamento (como fazer uma função para ordenar pelo primeiro nome de um objeto Pessoa, por exemplo).
Para fazer um ordenamento aleatório, nós temos a seguinte função: function () { return 0.5 – Math.random() }.
Math.random() retorna um número aleatório entre 0.0 e quase 1 (não chegando a, exatamente, 1). Desta maneira, há uma chance de 50% deste número ser maior do que 0.5 e 50% de ser menor do que 0.5. Ao fazer o retorno de 0.5 – Math.random() há, portanto, uma chance de 50% do resultado ser maior do que 0, e 50% deste número ser menor do que 0. Ou seja, a ordenação de cada dois elementos é definida aleatoriamente, que é o que nós queríamos
Alterando um atributo da carta
Nós já vimos como criar uma “folha” da árvore, mas muito de Javascript não consiste em criar esta folha e colocá-la no documento, mas também modificá-la conforme necessário.
Por exemplo, ao criarmos o elemento “img” nós não definimos seu atributo src, e agora? Para isso, nós podemos alterar atributos de qualquer ramo a qualquer hora, selecionando-o conforme necessário. E melhor, podemos mexer um atributo para todos os elementos de um determinado tipo, se for necessário. Por exemplo:
$(".carta img").attr("src","images/carta.png");
Aqui o que nós estamos selecionando são os elementos img que estejam sob o ramo “carta”. TODOS os elementos, veja que não há nenhuma restrição como first, last ou qualquer outra coisa. Então, nós podemos mexer em todos os elementos de uma só vez. E então nós usamos a função .attr(atributo,valor_do_atributo) para modificar um atributo de todos esses elementos. No caso, nós estamos definindo que o atributo src deve receber o valor “images/carta.png” que é o endereço do fundo da carta.
Aqui cabe uma ressalva: eu recomendo NÃO fazer como eu fiz e colocar esse endereço hardcoded como eu fiz. Crie uma variável e coloque o valor dela conforme for necessário, e use esta variável aqui. É que mais tarde você vai fazer uma comparação com este endereço e eu perdi algum tempo até perceber que o erro que eu estava tendo se devia ao fato de ter colocado um espaço a mais em um dos endereços da imagem. Vivendo e aprendendo :p
Os outras linhas se referem a definir que o número de pares que devem ser encontrados para terminar o jogo é o número total de pares do jogo, e depois se atribui que o número de tentativas é zero, e se atribui esse valor ao ramo “tentativas” da árvore. Pronto, temos um novo jogo.
Exceto que, por enquanto, ele não faz nada além de mostrar algumas cartas bonitinhas na tela. E agora?
A função mudarCarta()
Esta é a função responsável por mudar a carta quando ela é clicada:
function mudarCarta(elemento){
if ( ($(elemento).find("img").attr("src") == "images/carta.png" ) && (wait == 0))
{
$(elemento).find("img").attr("src", "images/" + cartaLadoB[$(".carta").index(elemento)]);
if (!is_segunda_carta) {
primeira_carta = elemento;
is_segunda_carta = true;
} else {
segunda_carta = elemento;
wait = 1;
setTimeout(verificaPar,700);
}
}
}
E aí, deu para perceber o que ela faz?
Elemento é a carta que foi clicada, que é passada como parâmetro desta função. A primeira coisa que é verificada é se é o fundo que está aparecendo, para evitar que alguém fique clicando numa imagem que já foi virada, e também verifica se o jogo não está esperando (wait == 0). Esperando pelo que? Calma, já vamos ver.
Se o jogo não estiver esperando e o jogador tiver clicado numa carta que ainda não foi virada, então é verificado qual a ordem da carta que foi clicada, para alterar para a imagem de acordo com que foi definido no array cartaLadoB. Novamente, vamos analisar esta linha de código com calma:
$(elemento).find("img").attr("src", "images/" + cartaLadoB[$(".carta").index(elemento)]);
Primeiro, nós pegamos o elemento e obtemos sua folha “img”, para então alterar seu atributo src. Ou seja, até $(elemento).find(“img”).attr(“src”, … está tranquilo entender, de acordo com que nós já vimos. É importante notar que .find() também é um seletor, logo, ele irá retornar todos os elementos que estejam de acordo com os parâmetros. No caso, teremos um único elemento img sendo retornado pois cada carta possui um único ramo chamado img.
E a imagem é definida de acordo com o array cartaLado B, de acordo com “images/” + cartaLadoB[$(".carta").index(elemento)]. O índice desejado do array é o índice da carta na lista de cartas da página. Mas como saber isso? Para isso, novamente usaremos seletores: primeiro selecionamos todos os elementos de classe carta com $(“.carta”) e então verificamos, nesta seleção, qual é o índice desse elemento que foi clicado com index(element0). Pronto, temos o índice correto da imagem a ser exibida
Feito isso, temos que verificar se esta foi a primeira carta clicada, ou seja, apenas deixe a carta exposta ali, ou se já é a segunda carta, para verificarmos o par e desvirar as duas cartas se for necessário. Isso é feito com o if (!is_segunda_carta) (que também poderia ser um if (is_primeira_carta), se você preferir :p), sendo que is_segunda_carta é uma variável booleana definida no início do documento javascript como false.
Se for a primeira carta, nós armazenamos esta carta como primeira_carta, para depois fazermos a comparação e, se for o caso, desvirá-la, e então mudamos a variável is_segunda_carta para que a próxima carta verifique o par conforme necessário.
Se já for a segunda carta, nós armazenamos essa carta na variável segunda_carta, definimos que devemos entrar em modo de espera (falei que iria explicar, não falei? :]) e chamamos a função verificaPar depois de 700 milissegundos. A função setTimeout faz justamente isso: chama uma determinada função, depois de um tempo pré-determinado. Algumas observações importantes sobre a função setTimeout, essa função malandra:
- você deve passar o NOME da função a ser chamada, e não invocá-la, por isso colocamos verificaPar e não verificaPar(). Se for necessário passar parâmetros, crie uma função anônima ali mesmo, com function() { funcaoQueVoceQuerChamar(parametros,que,voce,quiser);}.
- setTimeout não PÁRA o funcionamento ali. Ele joga a função para ser executada depois e continua executando as instruções do código. No caso, eu não coloquei nada depois de setTimeout mas, se existisse algo, esse algo seria executado antes de verificaPar ser executado.
Por causa desse tempo de espera eu criei a tal variável wait – ela está ali só para o jogador não ficar clicando nas cartas nesse tempo de espera. E a função é chamada só depois de 700 milissegundos para dar tempo do jogador ver a carta antes que ela seja desvirada.
A função verificaPar()
Estamos quase acabando, amiguinhos
Mas ainda falta o mais importante para o jogo funcionar: ele já vira as cartas, e tudo o mais, mas ainda não consegue verificar se você acertou o par ou não. E é aí que entra a última função desse joguinho, a verificaPar
function verificaPar(){
is_segunda_carta = false;
if ( $(segunda_carta).find("img").attr("src") == $(primeira_carta).find("img").attr("src"))
{
pares_restantes--;
} else {
$(segunda_carta).find("img").attr("src","images/carta.png");
$(primeira_carta).find("img").attr("src","images/carta.png");
}
tentativas++;
$("#tentativas").text(tentativas);
wait = 0;
if (pares_restantes == 0) {
novoJogo();
}
}
Eu acho que aqui, mesmo que no início você não soubesse nada de jQuery e Javascript, você já consegue entender o que esse código faz, não?
Mas, claro, como eu gosto de explicar as coisas (sério, eu ainda não sei se é uma coisa boa eu explicar tudo nos mínimos detalhes em todo tutorial… eu tento deixá-los claros e fáceis de ler, estou conseguindo amiguinhos? Ou só estou conseguindo fazer tutoriais enormes? :p), vamos olhar o que esse código está fazendo para não restar nenhuma dúvida.
Primeiro, nós definimos que a próxima carta não será uma segunda carta, isto é, is_segunda_carta volta a ser false. Depois, verificamos se a imagem mostrada na primeira e na segunda cartas são iguais.
Se forem iguais, é um a menos que o jogador deve encontrar.
Se fore diferentes, desvira as duas cartas, isto é, muda a imagem de volta para o fundo da carta.
No caso aumenta-se o número de tentativas e atualiza o número de tentativas mostrado na página no ramo “tentativas”.
Wait é definido como zero, isto é, o jogador pode voltar a clicar nas cartas.
Se não houverem mais pares a serem encontrados, isto é, pares_restantes for igual a zero, o jogo terminou e, portanto, deve-se iniciar um novo jogo.
E, é isso
Só para verificar, eu não cheguei a colocar explicitamente todas as variáveis que eu declarei no início de jogo.js, então aqui vão todas elas:
var pares = 4; var cartaLadoB = ["bear2-card.png","bear-card.png","leaf-card.png","leaf2-card.png", "bear2-card.png","bear-card.png","leaf-card.png","leaf2-card.png"]; var pares_restantes; var is_segunda_carta = false; var primeira_carta; var segunda_carta; var wait = 0; var tentativas; var mesa_de_jogo = new Object;
Lembrando que elas devem ser declaradas no início do documento (…eu acho).
E, bom, é isso, agora vocês tem um super hiper mega incrível demais estupendo fenomenal joguinho de memória, feito por vocês mesmos
UAU EIN
Aliás, sacanagem da semana foi eu estar toda empolgada com meu joguinho da memória e o Google me AVACALHAR com o joguinho do Pac-man feito em Javascript. Vou te contar, viu :p
Mas estou com o código salvo aqui (encontrei aqui) e vou estudá-lo com calma… não parece complexo, mas está bastante longo e foram usado algo para diminuir o tamanho do arquivo, acho, e várias variáveis estão com uma única letra como identificador, so, yeah.

