Cómo desarrollar un juego completo de "El quince" en Java

Cómo desarrollar un juego completo de "El quince" en Java

«Quince», o «Quince» es un excelente ejemplo de un juego lógico simple, popular en todo el mundo. Para resolver el rompecabezas, debes colocar los cuadrados con números en orden, de menor a mayor. No es fácil, pero es interesante.

En el tutorial de hoy mostramos cómo desarrollar «Quince» en Java 8 con Eclipse. Para el desarrollo de la interfaz de usuario utilizaremos el API Swing.

Recordamos: para todos los lectores de «Habr» — un descuento de 10,000 rublos al registrarse en cualquier curso de Skillbox usando el código promocional «Habr».

Skillbox recomienda: Curso en línea educativo «Profesión Desarrollador Java».

Diseño del juego

En esta etapa se deben definir las propiedades:

  • Size — tamaño del campo de juego;
  • nbTiles — número de quince en el campo. nbTiles = size*size — 1;
  • Tiles — quince, que representa un array unidimensional de números enteros. Cada uno de los quincen tendrá un valor único en el rango [0, nbTiles]. Cero representa el cuadrado vacío;
  • blankPos — posición del cuadrado vacío.

Lógica del juego

Es necesario definir el método reset, utilizado para inicializar una nueva posición del juego. Así establecemos el valor de cada elemento del array de quincen. Luego colocamos blankPos en la última posición del array.

También se necesita un método shuffle para mezclar el array de quincen. No incluimos el quince vacío en el proceso de mezclar, para dejarlo en su lugar.

Dado que solo la mitad de las posiciones iniciales posibles del rompecabezas tienen solución, es necesario verificar el resultado de la mezcla, para asegurarse de que la configuración actual sea resoluble. Para hacerlo, definimos el método isSolvable.

Si un quince está precedido por un quince de mayor valor, se considera una inversión. Cuando el quince vacío está en su lugar, el número de inversiones debe ser par para que el rompecabezas sea resoluble. Entonces, contamos el número de inversiones y devolvemos true si el número es par.

Luego, es importante definir el método isSolved para verificar si nuestra configuración del Juego de Quince está resuelta. Primero miramos dónde está el quince vacío. Si está en la posición inicial, entonces la configuración actual es nueva y no resuelta anteriormente. Luego, revisamos las fichas en orden inverso, y si el valor del quince difiere del índice correspondiente +1, devolvemos false. De lo contrario, al final del método es momento de devolver true, porque el rompecabezas ya está resuelto.

Otro método que debe definirse es newGame. Este es necesario para crear una nueva instancia del juego. Para ello, reiniciamos el tablero de juego, luego lo barajamos y continuamos hasta que la posición del juego sea resoluble.

Aquí hay un ejemplo de código con la lógica clave de las quince:

private void newGame() {
  do {
    reset(); // reiniciar al estado inicial
    shuffle(); // barajar
  } while(!isSolvable()); // continuar hasta que el tablero sea resoluble
 
  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;
  }
}
 
// Solo la mitad de las permutaciones del rompecabezas son resolubles/
// Cada vez que una loseta es precedida por una loseta de mayor valor cuenta
// como una inversión. En nuestro caso, con la loseta en blanco en la posición resuelta,
// el número de inversiones debe ser par para que el rompecabezas sea resoluble
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) // si la loseta en blanco no está en la posición resuelta ==> no resuelto
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Finalmente, debemos programar el movimiento de las quince en el arreglo. Este código será llamado más adelante mediante un callback para reaccionar al movimiento del cursor. Nuestro juego permitirá varios movimientos de losetas al mismo tiempo. Así, después de convertir la posición presionada en la pantalla a la loseta, obtenemos la posición de la loseta vacía y buscamos la dirección de movimiento para permitir varios movimientos simultáneos.

Aquí hay un ejemplo 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;

Desarrollando la interfaz de usuario con el API de Swing

Es hora de abordar la interfaz. Primero tomamos la clase Jpanel. Luego, dibujamos las quince en el área, utilizando los datos proporcionados en los parámetros del constructor del juego para calcular el tamaño de cada una:

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

El margen también es un parámetro proporcionado en el constructor del juego.

Ahora necesitamos definir el método drawGrid para dibujar la cuadrícula y las quince en la pantalla. Analizamos el arreglo de las quince y convertimos las coordenadas a coordenadas de la interfaz de usuario. Luego, dibujamos cada loseta con el número correspondiente en el centro:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // convertimos las coordenadas 1D a coordenadas 2D dada el tamaño del Array 2D
    int r = i / size;
    int c = i % size;
    // convertimos a coordenadas en la UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;
 
    // comprobamos el caso especial para la casilla en blanco
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }
 
      continue;
    }
 
    // para otras casillas
    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, redefiniremos el método paintComponent, que es un método derivado de la clase JPane. Luego utilizaremos el método drawGrid y, después, el método drawStartMessage para mostrar un mensaje que sugiera hacer clic para iniciar el juego:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Haz clic para iniciar un nuevo juego";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}
 
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centramos la cadena s para la casilla dada (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);
}

Reaccionamos a las acciones del usuario en la UI

Para que el juego funcione correctamente, es necesario manejar las acciones del usuario en la UI. Para ello, agregamos la implementación de MouseListener en Jpanel y el código para mover las piezas, ya mostrado anteriormente:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // utilizado para permitir que los usuarios interactúen en la cuadrícula haciendo clic
    // ¡es hora de implementar la interacción con los usuarios para mover las piezas y resolver el juego!
    if (gameOver) {
      newGame();
    } else {
      // obtenemos la posición del clic
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;
 
      // ¿clic en la cuadrícula?
      if (ex  gridSize  || ey  gridSize)
        return;
 
      // obtenemos la posición en la cuadrícula
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;
 
      // obtenemos la posición de la celda en blanco
      int c2 = blankPos % size;
      int r2 = blankPos / size;
 
      // convertimos a la coordenada 1D
      int clickPos = r1 * size + c1;
 
      int dir = 0;
 
      // buscamos dirección para múltiples movimientos de piezas a la vez
      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 las piezas en la dirección
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);
 
        tiles[blankPos] = 0;
      }
 
      // comprobamos si el juego está resuelto
      gameOver = isSolved();
    }
 
    // repintamos el panel
    repaint();
  }
});

El código se coloca en el constructor de la clase GameOfFifteen. Al final, llamamos al método newGame para iniciar un nuevo juego.

Código completo del juego

El último paso, antes de ver el juego en acción, es reunir todos los elementos del código. Así es como quedará:

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 Juego del Rompecabezas de 15 con Java 8 y Swing
// Si tienes alguna pregunta, no dudes en leer los comentarios ;)
public class GameOfFifteen extends JPanel { // nuestro grid se dibujará en un Panel dedicado
 
  // Tamaño de nuestra instancia del Juego de 15
  private int size;
  // Número de fichas
  private int nbTiles;
  // Dimensión de la UI del grid
  private int dimension;
  // Color de primer plano
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // usamos un color arbitrario
  // Objeto Random para mezclar las fichas
  private static final Random RANDOM = new Random();
  // Almacenando las fichas en un Array unidimensional de enteros
  private int[] tiles;
  // Tamaño de la ficha en la UI
  private int tileSize;
  // Posición de la ficha en blanco
  private int blankPos;
  // Margen para el grid en el marco
  private int margin;
  // Tamaño de la UI del grid
  private int gridSize;
  private boolean gameOver; // verdadero si el juego ha terminado, falso de lo contrario
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // inicializar fichas
    nbTiles = size * size - 1; // -1 porque no contamos la ficha en blanco
    tiles = new int[size * size];
    
    // calcular el tamaño del grid y el tamaño de la ficha
    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 a los usuarios interactuar con el grid haciendo clic
        // ¡es hora de implementar la interacción con los usuarios para mover las fichas y resolver el juego!
        if (gameOver) {
          newGame();
        } else {
          // obtener la posición del clic
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // clic en el grid?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // obtener posición en el grid
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // obtener la posición de la celda en blanco
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // convertimos a las coordenadas 1D
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // buscamos dirección para mover múltiples fichas a la vez
          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 las fichas en la dirección
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // verificamos si el juego está resuelto
          gameOver = isSolved();
        }
        
        // repintamos el panel
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // reinicia al estado inicial
      shuffle(); // mezcla
    } while(!isSolvable()); // continuamos hasta que el grid sea solucionable
    
    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;
    }
  }
 
  // Solo medio de las permutaciones del rompecabezas son solucionables.
  // Siempre que una ficha preceda a una ficha con un valor más alto, cuenta
  // como una inversión. En nuestro caso, con la ficha en blanco en la posición solucionada,
  // el número de inversiones debe ser par para que el rompecabezas sea solucionable
  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) // si la ficha en blanco no está en la posición resuelta => no resuelto
      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("Juego de los Quince");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centrar en la pantalla
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

¡Finalmente, jugamos!

Es el momento perfecto para iniciar el juego y probarlo en acción. El campo debería verse de la siguiente manera:

Cómo desarrollar un juego completo de "El quince" en Java

Intentamos resolver el rompecabezas. Si todo salió bien, obtenemos esto:

Cómo desarrollar un juego completo de "El quince" en Java

Eso es todo. ¿Esperabas algo más? 🙂

Skillbox recomienda:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster