"Quince" en Java: cómo desarrollar un juego completo



Quince, o Quince, es un gran ejemplo de un juego de lógica simple popular en todo el mundo. Para resolver el rompecabezas, debes organizar los cuadrados con números en orden, de menor a mayor. No es fácil, pero sí interesante.

En el tutorial de hoy, mostramos cómo desarrollar Fifteen en Java 8 con Eclipse. Para desarrollar la interfaz de usuario, utilizaremos la API Swing.

Le recordamos: para todos los lectores de "Habr": un descuento de 10.000 rublos al registrarse en cualquier curso de Skillbox con el código de promoción "Habr".

Skillbox recomienda: El curso educativo en línea "Profession Java-developer" .

Diseño del juego


En esta etapa, debe definir las propiedades:
  • Tamaño: el tamaño del campo de juego;
  • nbTiles: el número de puntos en el campo. nbTiles = tamaño * tamaño - 1;
  • Tiles es una etiqueta, que es una matriz unidimensional de enteros. Cada etiqueta recibirá un valor único en el rango [0, nbTiles]. Cero denota un cuadrado vacío;
  • blankPos: posición del cuadrado vacío.

Lógica del juego


Debes definir un método de reinicio utilizado para inicializar una nueva posición de juego. Entonces establecemos el valor para cada elemento de la matriz de etiquetas. Bueno, entonces colocamos blankPos en la última posición de la matriz.

También necesita el método aleatorio para mezclar una matriz de etiquetas. No incluimos una etiqueta vacía en el proceso de barajado para dejarla en su posición anterior.

Dado que solo la mitad de las posibles posiciones iniciales del rompecabezas tienen una solución, debe verificar el resultado de la mezcla resultante para asegurarse de que el diseño actual se resuelva en general. Para hacer esto, definimos el método isSolvable.

Si una etiqueta específica está precedida por una etiqueta con un valor más alto, esto se considera una inversión. Cuando la etiqueta vacía está en su lugar, el número de inversiones debe ser par para que el rompecabezas sea solucionable. Entonces, contamos el número de inversiones y devolvemos verdadero si el número es par.

Entonces es importante definir el método isSolved para verificar si nuestra alineación de Juego de Quince está resuelta. Primero, miramos dónde está la etiqueta vacía. Si está en la posición inicial, entonces la alineación actual es nueva, que no se ha decidido antes. Luego iteramos sobre los mosaicos en el orden inverso, y si el valor de la etiqueta difiere del índice +1 correspondiente, devuelve falso. De lo contrario, es hora de volver verdadero al final del método, porque el rompecabezas ya ha sido resuelto.

Otro método para definir es newGame. Se requiere para crear una nueva instancia del juego. Para hacer esto, reiniciamos el campo de juego, luego lo barajamos y continuamos hasta que la posición de juego sea resoluble.

Aquí hay un código de ejemplo con lógica de etiqueta de clave:
private void newGame() { do { reset(); // reset in initial state shuffle(); // shuffle } while(!isSolvable()); // make it until grid be solvable gameOver = false; } private void reset() { for (int i = 0; i < tiles.length; i++) { tiles[i] = (i + 1) % tiles.length; } // we set blank cell at the last blankPos = tiles.length - 1; } private void shuffle() { // don't include the blank tile in the shuffle, leave in the solved position int n = nbTiles; while (n > 1) { int r = RANDOM.nextInt(n--); int tmp = tiles[r]; tiles[r] = tiles[n]; tiles[n] = tmp; } } // Only half permutations of the puzzle are solvable/ // Whenever a tile is preceded by a tile with higher value it counts // as an inversion. In our case, with the blank tile in the solved position, // the number of inversions must be even for the puzzle to be solvable private boolean isSolvable() { int countInversions = 0; for (int i = 0; i < nbTiles; i++) { for (int j = 0; j < i; j++) { if (tiles[j] > tiles[i]) countInversions++; } } return countInversions % 2 == 0; } private boolean isSolved() { if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved return false; for (int i = nbTiles - 1; i >= 0; i--) { if (tiles[i] != i + 1) return false; } return true; } 

Finalmente, debe programar el movimiento de los puntos en la matriz. Este código se llamará más tarde mediante una devolución de llamada para responder al movimiento del cursor. Nuestro juego admitirá múltiples movimientos de fichas simultáneamente. Por lo tanto, después de convertir la posición presionada en la pantalla en una etiqueta, obtenemos la posición de una etiqueta vacía y buscamos la dirección del movimiento para admitir varios de sus movimientos al mismo tiempo.

Aquí hay un código de muestra:
 // 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; 

Desarrollamos IU en API Swing


Es hora de hacer la interfaz. Primero tomamos la clase Jpanel. Luego dibujamos puntos en el campo: para calcular los tamaños de cada uno, utilizaremos los datos especificados en el parámetro constructor del juego:
 gridSize = (dim -  2 * margin); tileSize = gridSize / size; 

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

Ahora debe definir el método drawGrid para dibujar la cuadrícula y los puntos en la pantalla. Analizamos la matriz de etiquetas y convertimos las coordenadas a las coordenadas de la interfaz de usuario. Luego dibuje cada etiqueta con el número correspondiente en el centro:
 private void drawGrid(Graphics2D g) { for (int i = 0; i < tiles.length; i++) { // we convert 1D coords to 2D coords given the size of the 2D Array int r = i / size; int c = i % size; // we convert in coords on the UI int x = margin + c * tileSize; int y = margin + r * tileSize; // check special case for blank tile if(tiles[i] == 0) { if (gameOver) { g.setColor(FOREGROUND_COLOR); drawCenteredString(g, "\u2713", x, y); } continue; } // for other tiles 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, redefinimos el método paintComponent, que es un derivado de la clase JPane. Luego usamos el método drawGrid, y luego usamos el método drawStartMessage para mostrar un mensaje que nos pide que hagamos 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 = "Click to start new game"; g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2, getHeight() - margin); } } private void drawCenteredString(Graphics2D g, String s, int x, int y) { // center string s for the given tile (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); } 

Respondemos a las acciones del usuario en la interfaz de usuario


Para que el juego siga su curso, debes procesar las acciones del usuario en la interfaz de usuario. Para hacer esto, agregue la implementación MouseListener en Jpanel y el código para mover los puntos que ya se muestran arriba:
 addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { // used to let users to interact on the grid by clicking // it's time to implement interaction with users to move tiles to solve the game ! if (gameOver) { newGame(); } else { // 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; } // we check if game is solved gameOver = isSolved(); } // we repaint panel repaint(); } }); 

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

Código de juego completo


El último paso, antes de ver el juego en acción, es reunir todos los elementos del código. Aquí está el resultado:
 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; // We are going to create a Game of 15 Puzzle with Java 8 and Swing // If you have some questions, feel free to read comments ;) public class GameOfFifteen extends JPanel { // our grid will be drawn in a dedicated Panel // Size of our Game of Fifteen instance private int size; // Number of tiles private int nbTiles; // Grid UI Dimension private int dimension; // Foreground Color private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // we use arbitrary color // Random object to shuffle tiles private static final Random RANDOM = new Random(); // Storing the tiles in a 1D Array of integers private int[] tiles; // Size of tile on UI private int tileSize; // Position of the blank tile private int blankPos; // Margin for the grid on the frame private int margin; // Grid UI Size private int gridSize; private boolean gameOver; // true if game over, false otherwise public GameOfFifteen(int size, int dim, int mar) { this.size = size; dimension = dim; margin = mar; // init tiles nbTiles = size * size - 1; // -1 because we don't count blank tile tiles = new int[size * size]; // calculate grid size and tile size 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) { // used to let users to interact on the grid by clicking // it's time to implement interaction with users to move tiles to solve the game ! if (gameOver) { newGame(); } else { // 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; } // we check if game is solved gameOver = isSolved(); } // we repaint panel repaint(); } }); newGame(); } private void newGame() { do { reset(); // reset in intial state shuffle(); // shuffle } while(!isSolvable()); // make it until grid be solvable gameOver = false; } private void reset() { for (int i = 0; i < tiles.length; i++) { tiles[i] = (i + 1) % tiles.length; } // we set blank cell at the last blankPos = tiles.length - 1; } private void shuffle() { // don't include the blank tile in the shuffle, leave in the solved position int n = nbTiles; while (n > 1) { int r = RANDOM.nextInt(n--); int tmp = tiles[r]; tiles[r] = tiles[n]; tiles[n] = tmp; } } // Only half permutations of the puzzle are solvable. // Whenever a tile is preceded by a tile with higher value it counts // as an inversion. In our case, with the blank tile in the solved position, // the number of inversions must be even for the puzzle to be solvable private boolean isSolvable() { int countInversions = 0; for (int i = 0; i < nbTiles; i++) { for (int j = 0; j < i; j++) { if (tiles[j] > tiles[i]) countInversions++; } } return countInversions % 2 == 0; } private boolean isSolved() { if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved 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 < tiles.length; i++) { // we convert 1D coords to 2D coords given the size of the 2D Array int r = i / size; int c = i % size; // we convert in coords on the UI int x = margin + c * tileSize; int y = margin + r * tileSize; // check special case for blank tile if(tiles[i] == 0) { if (gameOver) { g.setColor(FOREGROUND_COLOR); drawCenteredString(g, "\u2713", x, y); } continue; } // for other tiles 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); } } private void drawStartMessage(Graphics2D g) { if (gameOver) { g.setFont(getFont().deriveFont(Font.BOLD, 18)); g.setColor(FOREGROUND_COLOR); String s = "Click to start new game"; g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2, getHeight() - margin); } } private void drawCenteredString(Graphics2D g, String s, int x, int y) { // center string s for the given tile (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); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setTitle("Game of Fifteen"); frame.setResizable(false); frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER); frame.pack(); // center on the screen frame.setLocationRelativeTo(null); frame.setVisible(true); }); } } 

¡Por fin, juega!


Es hora de comenzar el juego y comprobarlo en acción. El campo debería verse así:



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



Eso es todo ¿Esperabas más? :)

Skillbox recomienda:

Source: https://habr.com/ru/post/445758/


All Articles