2048

0

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 anteriores2048
Física de corpo rígidoLógica de matriz (grid)
Velocidade e trajetóriaÍndices de linha e coluna
Colisão entre spritesFusão de valores numéricos
update() em 60 fpsupdate() responde ao input
Objetos no espaço 2DEstrutura de dados (array[][])

A série completa

Pong
física · colisão
Breakout
grupos · destruição
Space Shooter
spawn · projéteis
Plataforma
gravidade · câmera
2048
matriz · lógica pura

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

Boot Menu Game2048Scene Alcançar 2048

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

2
4
0
0
2
0
0
0
8
4
0
0
0
0
0
0

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:

2
2
4
 
antes
← esquerda
4
4
 
 
depois

O algoritmo para cada linha tem três etapas obrigatórias nessa ordem:

  1. Compactar — remover os zeros e empilhar os valores
  2. Fundir — percorrer e combinar valores iguais adjacentes
  3. 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
Separação de responsabilidades

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.


1
Tile.js — o bloco visual

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.

src/objects/Tile.js
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
}
2
GridManager.js — a lógica da matriz novo

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.

src/systems/GridManager.js
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
  }
}
💡 O truque do inverter → mergeLine → inverter é elegante: você escreve a lógica de fusão apenas uma vez (para a esquerda) e reutiliza para a direita e para baixo. Isso reduz o código à metade e elimina bugs de inconsistência entre direções.
3
Game2048Scene — create() e drawGrid()

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.

src/scenes/Game2048Scene.js — create()
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)
        }
      }
    }
  }
}
4
update() — capturando e processando jogadas

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.

src/scenes/Game2048Scene.js — update()
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())
}
⚠️ O novo tile só deve ser gerado se o movimento realmente alterou o grid. Por isso o retorno booleano do 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).
5
Registrando no PhaserGame.jsx

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.

src/PhaserGame.jsx
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} />
}
💡 O 2048 é um ótimo exemplo de que o Phaser não precisa ser usado só para jogos com física. Ele é igualmente poderoso para puzzles, card games, turn-based RPGs e qualquer jogo orientado a estado e lógica.

Conceitos de programação aprendidos

Matriz bidimensional
Array[][]

Estrutura de dados central do jogo. Usada em mapas de tiles, tabuleiros, grades de qualquer tipo.

Algoritmo de fusão
filter + merge + pad

Compactar, fundir e completar — três etapas que transformam uma linha do grid a cada movimento.

Reutilização por inversão
reverse + merge + reverse

Escrever a lógica uma vez e reutilizá-la para outras direções via inversão do array.

Controle de estado
moved, won, gameOver

Flags booleanas que controlam o fluxo do jogo. Padrão fundamental em qualquer aplicação.

Geração procedural
spawnTile()

Escolher uma célula vazia aleatória e popular com um valor. Base de geração de conteúdo em jogos.

Separação MVC
GridManager vs Scene

Lógica (model) separada da visão (view). O GridManager funciona sem o Phaser — testável de forma isolada.


Desafios para evoluir o jogo

🌞 Animações
  • Tiles deslizam suavemente
  • Efeito de escala ao fundir
  • Fade in no novo tile
🎉 Efeitos
  • Partícula ao atingir 2048
  • Shake de tela no game over
  • Sons de fusão e vitória
🏆 Modos
  • Grid 5×5 ou 6×6
  • Modo contra o tempo
  • Salvar maior pontuação
📱 Touch
  • Suporte a swipe mobile
  • Detectar direção do arraste
  • Feedback tátil
🚀 Conclusão da série: com o 2048 você completou uma jornada que cobre os fundamentos de praticamente todo jogo existente — física (Pong), destruição de objetos (Breakout), combate e spawn (Space Shooter), gravidade e câmera (Plataforma) e lógica e estrutura de dados (2048). A base está formada.