«Puzzle de quinze» em Java — como desenvolver um jogo completo

«Puzzle de quinze» em Java — como desenvolver um jogo completo

«Quinze», ou «Puzzle de quinze» — é um ótimo exemplo de um jogo lógico simples, popular em todo o mundo. Para resolver o quebra-cabeça, é necessário organizar os quadrados com os números em ordem, do menor para o maior. Isso não é fácil, mas é interessante.

No tutorial de hoje, mostramos como desenvolver o «Puzzle de quinze» em Java 8 com Eclipse. Para desenvolver a interface do usuário, usaremos a API Swing.

Lembramos: para todos os leitores do «Habr» — desconto de 10.000 rublos na inscrição em qualquer curso Skillbox com o código promocional «Habr».

A Skillbox recomenda: Curso online educacional «Profissão: Desenvolvedor Java».

Projetando o jogo

Nesta etapa, é preciso definir as propriedades:

  • Size — o tamanho do campo de jogo;
  • nbTiles — o número de peças no campo. nbTiles = size*size — 1;
  • Tiles — o puzzle, que consiste em um array unidimensional de inteiros. Cada peça receberá um valor único na faixa [0, nbTiles]. O zero representa o quadrado vazio;
  • blankPos — a posição do quadrado vazio.

Lógica do jogo

É necessário definir o método reset, usado para inicializar uma nova posição do jogo. Assim, definimos o valor para cada elemento do array de peças. Então, colocamos blankPos na última posição do array.

Também precisamos de um método shuffle para embaralhar o array de peças. Não incluímos a peça vazia no processo de embaralhamento, para mantê-la em sua posição original.

Como apenas metade das possíveis posições iniciais do quebra-cabeça tem solução, é necessário verificar o resultado do embaralhamento para garantir que a disposição atual seja solucionável. Para fazer isso, definimos o método isSolvable.

Se uma peça específica é precedida por uma peça de valor mais alto, isso é considerado uma inversão. Quando a peça vazia está em sua posição correta, o número de inversões deve ser par para que o quebra-cabeça seja solucionável. Portanto, contamos o número de inversões e retornamos verdadeiro se o número for par.

Em seguida, é importante definir o método isSolved para verificar se a nossa disposição do Game Of Fifteen está resolvida. Primeiro, olhamos onde está o bloco vazio. Se estiver na posição inicial, então a disposição atual é nova e não foi resolvida anteriormente. Depois, percorremos os blocos na ordem inversa e, se o valor do bloco for diferente do índice correspondente +1, retornamos false. Caso contrário, ao final do método, é hora de retornar true, pois o quebra-cabeça já está resolvido.

Outro método que precisa ser definido é o newGame. Ele é necessário para criar uma nova instância do jogo. Para isso, redefinimos o campo de jogo, embaralhamos e continuamos até que a posição do jogo seja solucionável.

Aqui está um exemplo de código com a lógica-chave do quebra-cabeça:

private void newGame() {
  do {
    reset(); // redefine para o estado inicial
    shuffle(); // embaralha
  } while(!isSolvable()); // faz até que a grade se torne solucionável
 
  gameOver = false;
}
 
private void reset() {
  for (int i = 0; i  1) {
    int r = RANDOM.nextInt(n--);
    int tmp = tiles[r];
    tiles[r] = tiles[n];
    tiles[n] = tmp;
  }
}
 
// Apenas metade das permutações do quebra-cabeça são solucionáveis/
// Sempre que um bloco é precedido por um bloco de maior valor, conta
// como uma inversão. No nosso caso, com o bloco vazio na posição resolvida,
// o número de inversões deve ser par para que o quebra-cabeça seja solucionável
private boolean isSolvable() {
  int countInversions = 0;
 
  for (int i = 0; i < nbTiles; i++) {
    for (int j = 0; j  tiles[i])
        countInversions++;
    }
  }
 
  return countInversions % 2 == 0;
}
 
private boolean isSolved() {
  if (tiles[tiles.length - 1] != 0) // se o bloco vazio não estiver na posição resolvida ==> não resolvido
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Por fim, precisamos programar o movimento dos blocos no array. Este código será chamado mais tarde por meio de um callback, para reagir ao movimento do cursor. Nosso jogo suportará múltiplos movimentos de blocos simultaneamente. Assim, depois de converter a posição pressionada na tela em um bloco, obtemos a posição do bloco vazio e buscamos a direção do movimento para suportar vários movimentos ao mesmo tempo.

Aqui está um exemplo de código:

// get position of the click
int ex = e.getX() - margin;
int ey = e.getY() - margin;
 
// click in the grid ?
if (ex < 0 || ex > gridSize  || ey < 0  || ey > gridSize)
  return;
 
// get position in the grid
int c1 = ex / tileSize;
int r1 = ey / tileSize;
 
// get position of the blank cell
int c2 = blankPos % size;
int r2 = blankPos / size;
 
// we convert in the 1D coord
int clickPos = r1 * size + c1;
 
int dir = 0;
 
// we search direction for multiple tile moves at once
if (c1 == c2  &&  Math.abs(r1 - r2) > 0)
  dir = (r1 - r2) > 0 ? size : -size;
else if (r1 == r2 && Math.abs(c1 - c2) > 0)
  dir = (c1 - c2) > 0 ? 1 : -1;
 
if (dir != 0) {
  // we move tiles in the direction
  do {
    int newBlankPos = blankPos + dir;
    tiles[blankPos] = tiles[newBlankPos];
    blankPos = newBlankPos;
  } while(blankPos != clickPos);
 
tiles[blankPos] = 0;

Desenvolvendo UI na API Swing

É hora de trabalhar na interface. Primeiro, pegamos a classe JPanel. Em seguida, desenhamos os blocos no campo — para calcular as dimensões de cada um, utilizaremos os dados fornecidos nos parâmetros do construtor do jogo:

gridSize = (dim - 2 * margin);
tileSize = gridSize / size;

A margem também é um parâmetro definido no construtor do jogo.

Agora precisamos definir o método drawGrid para desenhar a grade e as peças no painel. Analisamos o array das peças e convertemos as coordenadas para as coordenadas da interface do usuário. Em seguida, vamos desenhar cada peça com o número correspondente no centro:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // convertemos coordenadas 1D para 2D dado o tamanho do Array 2D
    int r = i / size;
    int c = i % size;
    // convertemos as coordenadas na interface do usuário
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // verifica caso especial para a peça em branco
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // para outras peças
    g.setColor(getForeground());
    g.fillRoundRect(x, y, tileSize, tileSize, 25, 25);
    g.setColor(Color.BLACK);
    g.drawRoundRect(x, y, tileSize, tileSize, 25, 25);
    g.setColor(Color.WHITE);

    drawCenteredString(g, String.valueOf(tiles[i]), x , y);
  }
}

Finalmente, vamos sobrepôr o método paintComponent, que é derivado da classe JPane. Em seguida, usamos o método drawGrid, e depois o método drawStartMessage para exibir uma mensagem sugerindo clicar para iniciar o jogo:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Clique para iniciar um novo jogo";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centraliza a string s para o tile dado (x,y)
  FontMetrics fm = g.getFontMetrics();
  int asc = fm.getAscent();
  int desc = fm.getDescent();
  g.drawString(s,  x + (tileSize - fm.stringWidth(s)) / 2,
      y + (asc + (tileSize - (asc + desc)) / 2));
}

@Override
protected void paintComponent(Graphics g) {
  super.paintComponent(g);
  Graphics2D g2D = (Graphics2D) g;
  g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  drawGrid(g2D);
  drawStartMessage(g2D);
}

Reagimos às ações do usuário na interface.

Para que o jogo funcione, é necessário processar as ações do usuário na interface. Para isso, adicionamos a implementação do MouseListener ao Jpanel e o código para mover as peças, já mostrado acima:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // usado para permitir que os usuários interajam na grade clicando
    // é hora de implementar a interação com os usuários para mover os blocos e resolver o jogo!
    if (gameOver) {
      newGame();
    } else {
      // obter a posição do clique
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;
 
      // clique na grade?
      if (ex  gridSize  || ey  gridSize)
        return;
 
      // obter a posição na grade
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;
 
      // obter a posição da célula em branco
      int c2 = blankPos % size;
      int r2 = blankPos / size;
 
      // convertemos na coord 1D
      int clickPos = r1 * size + c1;
 
      int dir = 0;
 
      // procuramos a direção para mover múltiplos blocos ao mesmo tempo
      if (c1 == c2  &&  Math.abs(r1 - r2) > 0)
        dir = (r1 - r2) > 0 ? size : -size;
      else if (r1 == r2 && Math.abs(c1 - c2) > 0)
        dir = (c1 - c2) > 0 ? 1 : -1;
 
      if (dir != 0) {
        // movemos os blocos na direção
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);
 
        tiles[blankPos] = 0;
      }
 
      // verificamos se o jogo está resolvido
      gameOver = isSolved();
    }
 
    // repintamos o painel
    repaint();
  }
});

O código é colocado no construtor da classe GameOfFifteen. No final, chamamos o método newGame para iniciar um novo jogo.

Código completo do jogo

O último passo, antes de ver o jogo em ação, é juntar todos os elementos do código. Veja o que teremos:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Random;
 
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
 
// Vamos a crear un Jogo do 15 com Java 8 e Swing
// Se você tiver alguma dúvida, sinta-se à vontade para ler os comentários ;)
public class GameOfFifteen extends JPanel { // nossa grade será desenhada em um painel dedicado
 
  // Tamanho da nossa instância do Jogo do 15
  private int size;
  // Número de peças
  private int nbTiles;
  // Dimensão da UI da grade
  private int dimension;
  // Cor de primeiro plano
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // usamos uma cor arbitrária
  // Objeto aleatório para embaralhar as peças
  private static final Random RANDOM = new Random();
  // Armazenando as peças em um array 1D de inteiros
  private int[] tiles;
  // Tamanho da peça na UI
  private int tileSize;
  // Posição da peça em branco
  private int blankPos;
  // Margem para a grade no quadro
  private int margin;
  // Tamanho da UI da grade
  private int gridSize;
  private boolean gameOver; // verdadeiro se o jogo acabou, falso caso contrário
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // Inicializar peças
    nbTiles = size * size - 1; // -1 porque não contamos a peça em branco
    tiles = new int[size * size];
    
    // Calcular tamanho da grade e tamanho da peça
    gridSize = (dim - 2 * margin);
    tileSize = gridSize / size;
    
    setPreferredSize(new Dimension(dimension, dimension + margin));
    setBackground(Color.WHITE);
    setForeground(FOREGROUND_COLOR);
    setFont(new Font("SansSerif", Font.BOLD, 60));
    
    gameOver = true;
    
    addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed(MouseEvent e) {
        // usado para permitir que os usuários interajam na grade clicando
        // é hora de implementar a interação com os usuários para mover as peças e resolver o jogo!
        if (gameOver) {
          newGame();
        } else {
          // obter posição do clique
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // clique na grade?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // obter posição na grade
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // obter posição da célula em branco
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // convertemos para as coordenadas 1D
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // procuramos a direção para múltiplos movimentos de peças ao mesmo tempo
          if (c1 == c2  &&  Math.abs(r1 - r2) > 0)
            dir = (r1 - r2) > 0 ? size : -size;
          else if (r1 == r2 && Math.abs(c1 - c2) > 0)
            dir = (c1 - c2) > 0 ? 1 : -1;
            
          if (dir != 0) {
            // movemos as peças na direção
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // checamos se o jogo está resolvido
          gameOver = isSolved();
        }
        
        // repintamos o painel
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // resetar para o estado inicial
      shuffle(); // embaralhar
    } while(!isSolvable()); // fazer isso até a grade ser solucionável
    
    gameOver = false;
  }
 
  private void reset() {
    for (int i = 0; i  1) {
      int r = RANDOM.nextInt(n--);
      int tmp = tiles[r];
      tiles[r] = tiles[n];
      tiles[n] = tmp;
    }
  }
 
  // Apenas metade das permutações do quebra-cabeça são solucionáveis.
  // Sempre que uma peça é precedida por uma peça com valor maior, conta
  // como uma inversão. No nosso caso, com a peça em branco na posição resolvida,
  // o número de inversões deve ser par para que o quebra-cabeça seja solucionável
  private boolean isSolvable() {
    int countInversions = 0;
    
    for (int i = 0; i < nbTiles; i++) {
      for (int j = 0; j  tiles[i])
          countInversions++;
      }
    }
    
    return countInversions % 2 == 0;
  }
 
  private boolean isSolved() {
    if (tiles[tiles.length - 1] != 0) // se a peça em branco não está na posição resolvida ==> não resolvido
      return false;
    
    for (int i = nbTiles - 1; i >= 0; i--) {
      if (tiles[i] != i + 1)
        return false;      
    }
    
    return true;
  }
 
  private void drawGrid(Graphics2D g) {
    for (int i = 0; i  {
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setTitle("Jogo do 15");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centralize na tela
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 

}

Finalmente, vamos jogar!

É hora de iniciar o jogo e testá-lo em ação. O campo deve parecer da seguinte maneira:

«Puzzle de quinze» em Java — como desenvolver um jogo completo

Vamos tentar resolver o quebra-cabeça. Se tudo correr bem, teremos o seguinte resultado:

«Puzzle de quinze» em Java — como desenvolver um jogo completo

E é isso. Você esperava mais? 🙂

A Skillbox recomenda:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster