2048
Voltar para: Games com Phaser + Reactjs
O 2048 é o primeiro jogo da série que não depende de física ou colisão — ele é puro raciocínio lógico sobre uma matriz. Isso o torna um projeto essencial: você aprende a pensar em estruturas de dados, estado de jogo e algoritmos de transformação que aparecem em todo tipo de software, não apenas em jogos.
📚 Curiosidade: o 2048 foi criado por Gabriele Cirulli em 2014, em um fim de semana, como um experimento pessoal. Em poucos dias se tornou um fenômeno viral com milhões de jogadores. O código original foi publicado abertamente no GitHub — uma das histórias mais conhecidas de indie game que viralizou sem marketing.
O que muda neste projeto?
Todos os jogos anteriores dependiam da física arcade do Phaser — gravidade, velocidade, colisão. O 2048 é diferente: o movimento acontece em uma matriz 4×4 controlada por lógica pura. O Phaser aqui serve apenas para renderizar os blocos e capturar o teclado.
| Jogos anteriores | 2048 |
|---|---|
| Física de corpo rígido | Lógica de matriz (grid) |
| Velocidade e trajetória | Índices de linha e coluna |
| Colisão entre sprites | Fusão de valores numéricos |
| update() em 60 fps | update() responde ao input |
| Objetos no espaço 2D | Estrutura de dados (array[][]) |
A série completa
Objetivos do projeto
- Grid 4×4 representado como matriz bidimensional
- Movimentação em quatro direções com teclado
- Fusão de blocos iguais ao colidirem
- Geração aleatória de novo bloco após cada jogada
- Sistema de pontuação acumulada
- Condição de vitória ao atingir o bloco 2048
Fluxo do jogo
Como o grid funciona
O tabuleiro é uma matriz 4×4 onde 0 representa espaço vazio e qualquer outro número representa um bloco:
representação lógica
[ 2 ][ 4 ][ 0 ][ 0 ] [ 2 ][ 0 ][ 0 ][ 0 ] [ 8 ][ 4 ][ 0 ][ 0 ] [ 0 ][ 0 ][ 0 ][ 0 ]
visual na tela
A lógica de fusão (merge)
Ao pressionar uma direção, todos os blocos deslizam para aquele lado. Blocos com o mesmo valor que se encontram se fundem em um único bloco com o dobro do valor:
O algoritmo para cada linha tem três etapas obrigatórias nessa ordem:
- Compactar — remover os zeros e empilhar os valores
- Fundir — percorrer e combinar valores iguais adjacentes
- Completar — preencher as posições restantes com zero
Estrutura de arquivos
src/ ├── main.jsx ├── PhaserGame.jsx ├── scenes/ │ ├── BootScene.js │ ├── MenuScene.js │ └── Game2048Scene.js ├── objects/ │ └── Tile.js # bloco visual com valor └── systems/ └── GridManager.js # toda a lógica da matriz
O GridManager não sabe nada sobre sprites ou Phaser — ele só manipula o array. O Game2048Scene não sabe nada sobre lógica de fusão — ele só lê o array e desenha. Essa separação é o padrão MVC aplicado a jogos e torna o código testável e reutilizável.
O Tile encapsula a representação visual de cada bloco: um retângulo colorido e um texto sobreposto. Ele também define a cor certa para cada valor, tornando o jogo visualmente mais fiel ao original.
export default class Tile { constructor(scene, x, y, value) { this.scene = scene this.value = value // Fundo do bloco com a cor correspondente ao valor this.box = scene.add.rectangle(x, y, 100, 100, colorFor(value)) this.box.setStrokeStyle(2, 0xbbada0) // Texto central com o número this.text = scene.add.text(x, y, value, { fontSize: value >= 100 ? '26px' : '34px', fontFamily: 'monospace', fontStyle: 'bold', color: value <= 4 ? '#776e65' : '#f9f6f2' }).setOrigin(0.5) } updateValue(value) { this.value = value this.text.setText(value) this.text.setFontSize(value >= 100 ? '26px' : '34px') this.text.setColor(value <= 4 ? '#776e65' : '#f9f6f2') this.box.setFillStyle(colorFor(value)) } destroy() { this.box.destroy() this.text.destroy() } } // Paleta de cores fiel ao jogo original function colorFor(value) { const palette = { 2: 0xeee4da, 4: 0xede0c8, 8: 0xf2b179, 16: 0xf59563, 32: 0xf67c5f, 64: 0xf65e3b, 128: 0xedcf72, 256: 0xedcc61, 512: 0xedc850, 1024: 0xedc53f, 2048: 0xedc22e, } return palette[value] || 0x3c3a32 }
O GridManager é o coração do jogo. Ele mantém o array 4×4, implementa a lógica de fusão por linha e expõe métodos para mover em cada direção. Nenhuma referência ao Phaser existe aqui — é lógica pura.
export default class GridManager { constructor() { this.size = 4 this.score = 0 this.won = false // Inicializa a matriz 4x4 com zeros this.grid = Array.from({ length: 4 }, () => new Array(4).fill(0) ) } // ─── Spawn ──────────────────────────────────────────── spawnTile() { // Coleta todas as células vazias const empty = [] for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (this.grid[r][c] === 0) empty.push({ r, c }) } } if (empty.length === 0) return false // Escolhe uma célula vazia aleatória const i = Math.floor(Math.random() * empty.length) const { r, c } = empty[i] // 90% chance de gerar 2, 10% de gerar 4 this.grid[r][c] = Math.random() < 0.9 ? 2 : 4 return true } // ─── Lógica de uma linha (move para a esquerda) ─────── mergeLine(line) { // 1. Compactar: remover zeros let row = line.filter(v => v !== 0) // 2. Fundir: combinar valores iguais adjacentes for (let i = 0; i < row.length - 1; i++) { if (row[i] === row[i + 1]) { row[i] *= 2 // dobra o valor this.score += row[i] // pontua if (row[i] === 2048) this.won = true row.splice(i + 1, 1) // remove o bloco fundido } } // 3. Completar com zeros à direita while (row.length < 4) row.push(0) return row } // ─── Movimentos (rotacionam o grid e aplicam mergeLine) ─ moveLeft() { let moved = false for (let r = 0; r < 4; r++) { const merged = this.mergeLine([...this.grid[r]]) if (merged.join() !== this.grid[r].join()) moved = true this.grid[r] = merged } return moved } moveRight() { let moved = false for (let r = 0; r < 4; r++) { // Inverte → aplica mergeLine → inverte de volta const reversed = [...this.grid[r]].reverse() const merged = this.mergeLine(reversed).reverse() if (merged.join() !== this.grid[r].join()) moved = true this.grid[r] = merged } return moved } moveUp() { return this.moveVertical(false) } moveDown() { return this.moveVertical(true) } moveVertical(reverse) { let moved = false for (let c = 0; c < 4; c++) { // Extrai a coluna como array let col = [0,1,2,3].map(r => this.grid[r][c]) if (reverse) col.reverse() const merged = this.mergeLine(col) if (reverse) merged.reverse() // Reescreve os valores na coluna original merged.forEach((v, r) => { if (this.grid[r][c] !== v) { this.grid[r][c] = v; moved = true } }) } return moved } // Verifica se ainda há movimentos possíveis hasMovesLeft() { for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (this.grid[r][c] === 0) return true if (c < 3 && this.grid[r][c] === this.grid[r][c+1]) return true if (r < 3 && this.grid[r][c] === this.grid[r+1][c]) return true } } return false } }
A cena inicializa o GridManager, spawn os dois blocos iniciais e desenha o tabuleiro. A função drawGrid() é chamada toda vez que o estado da matriz muda.
import GridManager from '../systems/GridManager' import Tile from '../objects/Tile' export default class Game2048Scene 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, 0xfaf8ef) // Fundo do tabuleiro this.add.rectangle(W/2, H/2 + 20, 460, 460, 0xbbada0) .setStrokeStyle(0) // Células vazias (decorativas) const sx = W/2 - 165, sy = H/2 - 145 for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { this.add.rectangle(sx + c * 110, sy + r * 110, 100, 100, 0xcdc1b4) } } // GridManager: lógica pura sem Phaser this.gm = new GridManager() this.gm.spawnTile() this.gm.spawnTile() // Array de Tiles visuais this.tiles = [] this.drawGrid() // Placar this.add.text(W/2, 36, '2048', { fontSize: '36px', fontFamily: 'monospace', fontStyle: 'bold', color: '#776e65' }).setOrigin(0.5) this.scoreText = this.add.text(W/2, 72, 'Pontos: 0', { fontSize: '16px', fontFamily: 'monospace', color: '#776e65' }).setOrigin(0.5) // Controles this.cursors = this.input.keyboard.createCursorKeys() this.wasd = this.input.keyboard.addKeys({ up: 'W', down: 'S', left: 'A', right: 'D' }) this.gameOver = false } // Redesenha todos os tiles com base no estado atual do grid drawGrid() { // Destrói todos os tiles existentes this.tiles.forEach(t => t.destroy()) this.tiles = [] const { width: W, height: H } = this.cameras.main const sx = W/2 - 165, sy = H/2 - 145 for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { const value = this.gm.grid[r][c] if (value !== 0) { const tile = new Tile( this, sx + c * 110, sy + r * 110, value ) this.tiles.push(tile) } } } } }
O update() do 2048 é diferente dos jogos anteriores: ele não precisa rodar a 60fps com lógica contínua. Ele apenas aguarda uma tecla ser pressionada, aplica o movimento e redesenha o tabuleiro.
update() { if (this.gameOver) return let moved = false const left = Phaser.Input.Keyboard.JustDown(this.cursors.left) || Phaser.Input.Keyboard.JustDown(this.wasd.left) const right = Phaser.Input.Keyboard.JustDown(this.cursors.right) || Phaser.Input.Keyboard.JustDown(this.wasd.right) const up = Phaser.Input.Keyboard.JustDown(this.cursors.up) || Phaser.Input.Keyboard.JustDown(this.wasd.up) const down = Phaser.Input.Keyboard.JustDown(this.cursors.down) || Phaser.Input.Keyboard.JustDown(this.wasd.down) if (left) moved = this.gm.moveLeft() else if (right) moved = this.gm.moveRight() else if (up) moved = this.gm.moveUp() else if (down) moved = this.gm.moveDown() else return // nenhuma tecla pressionada: não faz nada if (moved) { // Só gera novo tile se algo se moveu this.gm.spawnTile() this.drawGrid() this.scoreText.setText('Pontos: ' + this.gm.score) // Verificações de fim de jogo if (this.gm.won) this.showEndScreen(true) else if (!this.gm.hasMovesLeft()) this.showEndScreen(false) } } showEndScreen(won) { this.gameOver = true const { width: W, height: H } = this.cameras.main // Overlay semitransparente this.add.rectangle(W/2, H/2, W, H, 0xfaf8ef, 0.7) this.add.text(W/2, H/2 - 40, won ? 'Você venceu!' : 'Game Over', { fontSize: '44px', fontFamily: 'monospace', fontStyle: 'bold', color: won ? '#f65e3b' : '#776e65' }).setOrigin(0.5) this.add.text(W/2, H/2 + 20, 'Pontos: ' + this.gm.score, { fontSize: '22px', fontFamily: 'monospace', color: '#776e65' }).setOrigin(0.5) this.add.text(W/2, H/2 + 70, 'ESPAÇO = jogar de novo', { fontSize: '14px', fontFamily: 'monospace', color: '#aaa' }).setOrigin(0.5) this.input.keyboard .addKey('SPACE').on('down', () => this.scene.restart()) }
moveLeft/Right/Up/Down é importante — evita gerar blocos em jogadas que não mudaram nada (como tentar ir para a esquerda quando já está tudo à esquerda).A integração com React é idêntica aos projetos anteriores. O 2048 não usa física arcade, então o config é ainda mais simples — sem o bloco physics.
import { useEffect, useRef } from 'react' import Phaser from 'phaser' import BootScene from './scenes/BootScene' import MenuScene from './scenes/MenuScene' import Game2048Scene from './scenes/Game2048Scene' export default function PhaserGame() { const divRef = useRef(null) useEffect(() => { const config = { type: Phaser.AUTO, width: 520, height: 600, parent: divRef.current, backgroundColor: '#faf8ef', // Sem physics: o 2048 não usa física arcade scene: [BootScene, MenuScene, Game2048Scene] } const game = new Phaser.Game(config) return () => game.destroy(true) }, []) return <div ref={divRef} /> }
Conceitos de programação aprendidos
Estrutura de dados central do jogo. Usada em mapas de tiles, tabuleiros, grades de qualquer tipo.
Compactar, fundir e completar — três etapas que transformam uma linha do grid a cada movimento.
Escrever a lógica uma vez e reutilizá-la para outras direções via inversão do array.
Flags booleanas que controlam o fluxo do jogo. Padrão fundamental em qualquer aplicação.
Escolher uma célula vazia aleatória e popular com um valor. Base de geração de conteúdo em jogos.
Lógica (model) separada da visão (view). O GridManager funciona sem o Phaser — testável de forma isolada.
Desafios para evoluir o jogo
- Tiles deslizam suavemente
- Efeito de escala ao fundir
- Fade in no novo tile
- Partícula ao atingir 2048
- Shake de tela no game over
- Sons de fusão e vitória
- Grid 5×5 ou 6×6
- Modo contra o tempo
- Salvar maior pontuação
- Suporte a swipe mobile
- Detectar direção do arraste
- Feedback tátil
