Jogo da Memória
Voltar para: Games com Phaser + Reactjs
O Jogo da Memória é o próximo passo natural após o 2048 — ele combina gerenciamento de estado com lógica por turnos e adiciona um elemento novo: o jogador precisa lembrar o que viu. É o primeiro jogo da série com interação exclusivamente por clique do mouse, o que abre um novo capítulo no design de interfaces de jogo.
📚 Curiosidade: o Jogo da Memória foi patenteado por Ravensburger na Alemanha em 1959, mas variações do conceito existem há séculos em culturas diferentes. A versão digital ganhou popularidade como exercício cognitivo e é um dos primeiros projetos ensinados em cursos de programação web — porque ele exige exatamente o raciocínio sobre estado, eventos e temporização que separa iniciantes de desenvolvedores experientes.
O que este projeto introduz?
Assim como o 2048 abandonou a física por lógica de matriz, o Jogo da Memória adiciona um conceito que ainda não apareceu na série: estado por objeto individual. Cada carta tem seu próprio estado e o jogo reage à combinação de estados de duas cartas simultâneas.
| Projetos anteriores | Jogo da Memória |
|---|---|
| Input contínuo (teclado held) | Input pontual (clique do mouse) |
| Lógica roda em update() | Lógica roda em callbacks de evento |
| Objetos criados e destruídos | Objetos mudam de estado visual |
| Ação imediata ao input | Ação adiada com time.delayedCall |
| Estado global da partida | Estado individual por objeto |
A série completa
Objetivos do projeto
- Grade 4×4 com 16 cartas (8 pares)
- Cartas embaralhadas aleatoriamente a cada partida
- Clique para revelar carta, comparar duas por vez
- Par correto fica revelado; par errado vira de volta após pausa
- Contador de jogadas e cronômetro
- Tela de vitória ao encontrar todos os pares
Estados de uma carta
Cada carta pode estar em um de três estados. O jogo inteiro gira em torno de gerenciar esses estados:
Fluxo do jogo
Estrutura de arquivos
src/ ├── main.jsx ├── PhaserGame.jsx ├── scenes/ │ ├── BootScene.js │ ├── MenuScene.js │ └── MemoryScene.js ├── objects/ │ └── Card.js # carta com 3 estados visuais └── systems/ └── CardManager.js # embaralhamento e lógica de turnos
O CardManager cuida de toda a lógica — embaralhar, registrar cliques, comparar pares e verificar vitória. O Card cuida apenas de como cada carta aparece na tela. A MemoryScene conecta os dois e gerencia o cronômetro e o placar. Nenhum dos três sabe mais do que precisa sobre os outros.
A carta tem três representações visuais: verso (fundo colorido sem símbolo), frente (símbolo revelado) e par encontrado (destaque verde). O método setState troca entre elas sem recriar o objeto.
// Estados possíveis de uma carta export const CARD_STATE = { HIDDEN: 'hidden', REVEALED: 'revealed', MATCHED: 'matched' } export default class Card { constructor(scene, x, y, symbol, id) { this.scene = scene this.symbol = symbol // emoji ou letra do par this.id = id // índice único na grade this.state = CARD_STATE.HIDDEN this.locked = false // bloqueia clique durante animação // ─── Elementos visuais ──────────────────────────────── // Fundo da carta this.bg = scene.add .rectangle(x, y, 108, 108, 0x0F6E56) .setStrokeStyle(2, 0x085041) .setInteractive() // habilita clique // Símbolo (fica invisível enquanto hidden) this.label = scene.add .text(x, y, symbol, { fontSize: '36px', fontFamily: 'monospace' }) .setOrigin(0.5) .setAlpha(0) // começa invisível // Ícone de verso (fica visível enquanto hidden) this.icon = scene.add .text(x, y, '?', { fontSize: '32px', fontFamily: 'monospace', color: '#9FE1CB' }) .setOrigin(0.5) } // Registra o callback de clique vindo da cena onClick(callback) { this.bg.on('pointerdown', () => { if (this.locked || this.state !== CARD_STATE.HIDDEN) return callback(this) }) // Feedback visual de hover this.bg.on('pointerover', () => { if (this.state === CARD_STATE.HIDDEN) this.bg.setFillStyle(0x1D9E75) }) this.bg.on('pointerout', () => { if (this.state === CARD_STATE.HIDDEN) this.bg.setFillStyle(0x0F6E56) }) } setState(state) { this.state = state switch (state) { case CARD_STATE.HIDDEN: this.bg.setFillStyle(0x0F6E56) this.label.setAlpha(0) this.icon.setAlpha(1) break case CARD_STATE.REVEALED: this.bg.setFillStyle(0xE1F5EE) this.label.setAlpha(1) this.icon.setAlpha(0) break case CARD_STATE.MATCHED: this.bg.setFillStyle(0x9FE1CB) this.bg.setStrokeStyle(3, 0x1D9E75) this.label.setAlpha(1) this.icon.setAlpha(0) break } } destroy() { this.bg.destroy() this.label.destroy() this.icon.destroy() } }
setInteractive() habilita os eventos de ponteiro (clique, hover) em qualquer objeto gráfico do Phaser. Sem ele, on('pointerdown') simplesmente não dispara.O CardManager é responsável por três coisas: embaralhar os símbolos (Fisher-Yates shuffle), registrar qual carta foi clicada e comparar dois pares quando o segundo clique acontece.
import { CARD_STATE } from '../objects/Card' // 8 pares de símbolos — cada um aparece 2 vezes no deck const SYMBOLS = ['🌻','🍎','🌟','🌎','🚀','⚽','🆕','☕'] export default class CardManager { constructor(onMatch, onMismatch, onVictory) { this.onMatch = onMatch // callback: par correto this.onMismatch = onMismatch // callback: par errado this.onVictory = onVictory // callback: todos pares encontrados this.firstCard = null // primeira carta do turno this.locked = false // bloqueia durante a comparação this.matchCount = 0 // pares encontrados this.moves = 0 // jogadas realizadas // Cria e embaralha o deck: [🌻,🌻,🍎,🍎, ...] this.deck = shuffle([...SYMBOLS, ...SYMBOLS]) } // Chamado pela cena quando uma carta é clicada handleClick(card, scene) { if (this.locked) return card.setState(CARD_STATE.REVEALED) // ── Primeiro clique do turno ────────────────────────── if (!this.firstCard) { this.firstCard = card return } // ── Segundo clique: comparar ────────────────────────── this.moves++ const first = this.firstCard const second = card this.firstCard = null this.locked = true // bloqueia novos cliques if (first.symbol === second.symbol) { // ✔ Par correto first.setState(CARD_STATE.MATCHED) second.setState(CARD_STATE.MATCHED) first.locked = true second.locked = true this.matchCount++ this.locked = false this.onMatch(this.moves) if (this.matchCount === SYMBOLS.length) { this.onVictory(this.moves) } } else { // ✘ Par errado: aguarda 900ms e vira de volta scene.time.delayedCall(900, () => { first.setState(CARD_STATE.HIDDEN) second.setState(CARD_STATE.HIDDEN) this.locked = false this.onMismatch(this.moves) }) } } } // Fisher-Yates shuffle: embaralhamento imparcial function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[arr[i], arr[j]] = [arr[j], arr[i]] } return arr }
sort(() => Math.random() - 0.5)) produz distribuições estatisticamente enviesadas — alguns arranjos aparecem com mais frequência que outros. Fisher-Yates garante distribuição uniforme.A cena cria as 16 cartas em uma grade 4×4 e instancia o CardManager com os três callbacks. O update() desta cena é mínimo — quase toda a lógica vive nos callbacks de clique.
import Card from '../objects/Card' import CardManager from '../systems/CardManager' export default class MemoryScene extends Phaser.Scene { constructor() { super('Game') } create() { const { width: W, height: H } = this.cameras.main // Fundo this.add.rectangle(W/2, H/2, W, H, 0x041a14) // ─── CardManager com callbacks ──────────────────────── this.manager = new CardManager( (moves) => this.onMatch(moves), (moves) => this.onMismatch(moves), (moves) => this.showVictory(moves) ) // ─── Grade 4x4 de cartas ───────────────────────────── this.cards = [] const cols = 4, rows = 4 const cardW = 112, cardH = 112 const gapX = 8, gapY = 8 const totalW = cols * cardW + (cols - 1) * gapX const totalH = rows * cardH + (rows - 1) * gapY const startX = (W - totalW) / 2 + cardW / 2 const startY = 100 for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const id = row * cols + col const x = startX + col * (cardW + gapX) const y = startY + row * (cardH + gapY) const sym = this.manager.deck[id] const card = new Card(this, x, y, sym, id) // Passa o clique ao CardManager card.onClick((c) => this.manager.handleClick(c, this)) this.cards.push(card) } } // ─── HUD ───────────────────────────────────────────── this.add.text(W/2, 30, 'Jogo da Memória', { fontSize: '20px', fontFamily: 'monospace', fontStyle: 'bold', color: '#9FE1CB' }).setOrigin(0.5) this.movesText = this.add.text(16, H - 36, 'Jogadas: 0', { fontSize: '15px', fontFamily: 'monospace', color: '#9FE1CB' }) this.timerText = this.add.text(W - 16, H - 36, '0s', { fontSize: '15px', fontFamily: 'monospace', color: '#9FE1CB' }).setOrigin(1, 0) // ─── Cronômetro ─────────────────────────────────────── this.elapsed = 0 this.gameOver = false // Atualiza o cronômetro a cada segundo this.time.addEvent({ delay: 1000, loop: true, callback: () => { if (this.gameOver) return this.elapsed++ this.timerText.setText(this.elapsed + 's') } }) } onMatch(moves) { this.movesText.setText('Jogadas: ' + moves) } onMismatch(moves) { this.movesText.setText('Jogadas: ' + moves) // Shake rápido na câmera como feedback de erro this.cameras.main.shake(120, 0.004) } showVictory(moves) { this.gameOver = true const { width: W, height: H } = this.cameras.main // Overlay escuro semitransparente this.add.rectangle(W/2, H/2, W, H, 0x000000, 0.65) this.add.text(W/2, H/2 - 50, 'Parabéns!', { fontSize: '44px', fontFamily: 'monospace', fontStyle: 'bold', color: '#9FE1CB' }).setOrigin(0.5) this.add.text(W/2, H/2 + 10, moves + ' jogadas · ' + this.elapsed + 's', { fontSize: '20px', fontFamily: 'monospace', color: '#ffffff' }).setOrigin(0.5) this.add.text(W/2, H/2 + 65, 'ESPAÇO = jogar de novo', { fontSize: '13px', fontFamily: 'monospace', color: '#5DCAA5' }).setOrigin(0.5) this.input.keyboard .addKey('SPACE').on('down', () => this.scene.restart()) } update() { // Nada aqui — toda a lógica vive nos callbacks de clique } }
locked do CardManager é essencial. Sem ele, o jogador pode clicar em uma terceira carta durante os 900ms de pausa, corrompendo o estado do turno. Sempre bloqueie a entrada durante animações e transições assíncronas.Entender o fluxo de um turno é fundamental para não se perder nos callbacks. Cada jogada completa passa por estas etapas:
// Resumo do fluxo no CardManager: // Clique 1: apenas salva a referência if (!this.firstCard) { this.firstCard = card return // espera o segundo clique } // Clique 2: compara e decide o destino this.locked = true // ninguém mais clica agora if (first.symbol === second.symbol) // ✔ par correto marcarComoMatched() this.locked = false // libera imediatamente else // ✘ par errado aguardar900ms() virarDeVolta() this.locked = false // libera após a pausa
O Jogo da Memória também não usa física arcade. O config é simples — apenas adicione a cena à lista.
import { useEffect, useRef } from 'react' import Phaser from 'phaser' import BootScene from './scenes/BootScene' import MenuScene from './scenes/MenuScene' import MemoryScene from './scenes/MemoryScene' export default function PhaserGame() { const divRef = useRef(null) useEffect(() => { const config = { type: Phaser.AUTO, width: 560, height: 640, parent: divRef.current, backgroundColor: '#041a14', // Sem physics — o jogo é baseado em eventos de clique scene: [BootScene, MenuScene, MemoryScene] } const game = new Phaser.Game(config) return () => game.destroy(true) }, []) return <div ref={divRef} /> }
Conceitos fundamentais aprendidos
Ativa os eventos de ponteiro em qualquer sprite ou forma geométrica. Pré-requisito para qualquer clique.
Substitui o teclado como forma de input. Essencial para puzzles, card games e interfaces de jogo.
Cada carta tem seu próprio estado. Padrão de máquina de estados — base de animações e IA.
Impede interações durante transições assíncronas. Obrigatório em qualquer lógica com atraso de tempo.
Algoritmo estatisticamente correto para embaralhar arrays. Usado em jogos de carta, loot e mapas gerados.
Shake de câmera como resposta visual a um erro. Técnica de game feel usada em quase todo jogo moderno.
Desafios para evoluir o jogo
- Virar a carta com tween de escaleX
- Efeito de brilho ao encontrar par
- Fade in na tela de vitória
- Grade 6×6 (18 pares)
- Modo contra o tempo
- Dificuldades com tempo menor
- Salvar melhor tempo no localStorage
- Ranking de menor número de jogadas
- Histórico de partidas
- Som de virar carta
- Som diferente para par correto/errado
- Temas de símbolos (animais, países)
this.tweens.add({ targets: card.bg, scaleX: 0, duration: 150, onComplete: () => { ... } }). Escalar X até zero, trocar o conteúdo e escalar de volta cria a ilusão perfeita de uma carta virando — com apenas 10 linhas de código.