"Il gioco dei numeri" in Java - come sviluppare un gioco completo.

"Il gioco dei numeri" in Java - come sviluppare un gioco completo.

«Quindici», o «Quindicina» — un ottimo esempio di un semplice gioco logico, popolare in tutto il mondo. Per risolvere il rompicapo, è necessario disporre i quadrati con i numeri in ordine, dal più piccolo al più grande. Non è facile, ma è interessante.

Nel tutorial di oggi mostriamo come sviluppare «Quindicina» su Java 8 con Eclipse. Per lo sviluppo dell'interfaccia utente utilizzeremo Swing API.

Ricordiamo: per tutti i lettori di «Habr» — sconto di 10.000 rubli per l'iscrizione a qualsiasi corso Skillbox con il codice promozionale «Habr».

Skillbox consiglia: Corso online educativo «Professione Sviluppatore Java».

Progettazione del gioco

In questa fase è necessario definire le proprietà:

  • Size — dimensione del campo di gioco;
  • nbTiles — numero di tasselli nel campo. nbTiles = size*size — 1;
  • Tiles — quindicina, che rappresenta un array unidimensionale di numeri interi. Ognuna delle quindicine avrà un valore unico nell'intervallo [0, nbTiles]. Lo zero indica il quadrato vuoto;
  • blankPos — posizione del quadrato vuoto.

Logica del gioco

È necessario definire il metodo di ripristino (reset), utilizzato per inizializzare una nuova posizione di gioco. In questo modo impostiamo un valore per ciascun elemento dell'array delle quindicine. Poi posizioniamo blankPos nell'ultima posizione dell'array.

Servirà anche un metodo di mescolamento per mescolare l'array delle quindicine. Non includiamo la quindicina vuota nel processo di mescolamento per mantenerla nella sua posizione originale.

Poiché solo metà delle possibili posizioni iniziali del rompicapo ha una soluzione, è necessario verificare il risultato ottenuto dalla mescolanza per assicurarsi che la disposizione attuale sia effettivamente risolvibile. A tal fine, definiamo il metodo isSolvable.

Se una certa quindicina è preceduta da una quindicina con un valore più alto, questo è considerato un'inversione. Quando la quindicina vuota si trova al suo posto, il numero di inversioni deve essere pari affinché il rompicapo sia risolvibile. Quindi, contiamo il numero di inversioni e restituiamo true se il numero è pari.

È quindi importante definire il metodo isSolved per verificare se la nostra disposizione del Gioco dei Quindici è risolta. Iniziamo osservando dove si trova la quindicina vuota. Se nella posizione iniziale, allora la disposizione attuale è nuova, non risolta in precedenza. Poi scorriamo le tessere in ordine inverso e, se il valore della quindicina è diverso dall'indice corrispondente +1, restituiamo false. Altrimenti, alla fine del metodo, è giunto il momento di restituire true, perché il rompicapo è già risolto.

Un altro metodo da definire è newGame. Questo è necessario per creare una nuova istanza del gioco. Per fare ciò, ripristiniamo il campo di gioco, lo mescoliamo e continuiamo finché la posizione di gioco non sarà risolvibile.

Ecco un esempio di codice con la logica chiave dei quindici:

private void newGame() {
  do {
    reset(); // ripristina allo stato iniziale
    shuffle(); // mescola
  } while(!isSolvable()); // continua finché la griglia non è risolvibile
 
  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 metà delle permutazioni del puzzle sono risolvibili/
// Ogni volta che una tessera è preceduta da una tessera con un valore più alto, conta
// come un'inversione. Nel nostro caso, con la tessera vuota nella posizione risolta,
// il numero di inversioni deve essere pari affinché il puzzle sia risolvibile
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 la tessera vuota non è nella posizione risolta => non risolto
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Infine, dobbiamo programmare il movimento delle tessere nel array. Questo codice verrà chiamato in seguito tramite callback, per rispondere al movimento del cursore. Il nostro gioco supporterà più movimenti delle tessere simultaneamente. Quindi, dopo aver convertito la posizione toccata sullo schermo nella tessera, otteniamo la posizione della tessera vuota e cerchiamo la direzione del movimento per supportare più spostamenti contemporaneamente.

Ecco un esempio di codice:

// 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;

Sviluppiamo l'interfaccia utente con l'API Swing

È ora di lavorare sull'interfaccia. Per prima cosa prendiamo la classe JPanel. Poi disegniamo le tessere sul campo — per calcolare le dimensioni di ciascuna utilizzeremo i dati forniti nel parametro del costruttore del gioco:

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

Il margine è anche un parametro fornito nel costruttore del gioco.

Ora dobbiamo definire il metodo drawGrid per disegnare la griglia e le tessere sullo schermo. Analizziamo l'array delle tessere e convertiamo le coordinate in coordinate dell'interfaccia utente. Poi disegniamo ogni tessera con il numero corrispondente al centro:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // convertiamo le coordinate 1D in coordinate 2D date le dimensioni dell'array 2D
    int r = i / size;
    int c = i % size;
    // convertiamo in coordinate sull'interfaccia utente
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // controlla il caso speciale per la tessera vuota
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // per altre tessere
    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);
  }
}

Infine, ridefiniamo il metodo paintComponent, derivato dalla classe JPane. Poi utilizziamo il metodo drawGrid, seguito dal metodo drawStartMessage per visualizzare un messaggio che invita a cliccare per avviare il gioco:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Clicca per avviare un nuovo gioco";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centriamo la stringa s per la tessera data (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);
}

Reagiamo alle azioni degli utenti nell'interfaccia utente

Per garantire il corretto svolgimento del gioco, è necessario gestire le azioni degli utenti nell'interfaccia. A questo scopo, aggiungiamo un'implementazione di MouseListener su Jpanel e il codice per spostare i tasselli, già mostrato sopra:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // utilizzato per consentire agli utenti di interagire sulla griglia facendo clic
    // è tempo di implementare l'interazione con gli utenti per spostare i tasselli e risolvere il gioco !
    if (gameOver) {
      newGame();
    } else {
      // otteniamo la posizione del clic
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // clic sulla griglia ?
      if (ex  gridSize  || ey  gridSize)
        return;

      // otteniamo la posizione nella griglia
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // otteniamo la posizione della cella vuota
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // convertiamo in coordinata 1D
      int clickPos = r1 * size + c1;

      int dir = 0;

      // cerchiamo la direzione per spostare più tasselli contemporaneamente
      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) {
        // muoviamo i tasselli nella direzione
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // controlliamo se il gioco è risolto
      gameOver = isSolved();
    }

    // ridisegniamo il pannello
    repaint();
  }
});

Il codice va inserito nel costruttore della classe GameOfFifteen. Alla fine chiamiamo il metodo newGame per iniziare una nuova partita.

Codice completo del gioco

L'ultimo passo, prima di vedere il gioco in azione, è mettere insieme tutti gli elementi del codice. Ecco cosa otterremo:

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;
 
// Stiamo per creare un Gioco del 15 con Java 8 e Swing
// Se hai domande, sentiti libero di leggere i commenti ;)
public class GameOfFifteen extends JPanel { // la nostra griglia sarà disegnata in un pannello dedicato
 
  // Dimensione della nostra istanza del Gioco del 15
  private int size;
  // Numero di tessere
  private int nbTiles;
  // Dimensione UI della griglia
  private int dimension;
  // Colore di primo piano
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // usiamo un colore arbitrario
  // Oggetto Random per mescolare le tessere
  private static final Random RANDOM = new Random();
  // Memorizzazione delle tessere in un Array 1D di interi
  private int[] tiles;
  // Dimensione della tessera su UI
  private int tileSize;
  // Posizione della tessera vuota
  private int blankPos;
  // Margine per la griglia sul frame
  private int margin;
  // Dimensione UI della griglia
  private int gridSize;
  private boolean gameOver; // true se il gioco è finito, false altrimenti
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // inizializza le tessere
    nbTiles = size * size - 1; // -1 perché non contiamo la tessera vuota
    tiles = new int[size * size];
    
    // calcola la dimensione della griglia e la dimensione della tessera
    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) {
        // usato per far interagire gli utenti sulla griglia facendo clic
        // è tempo di implementare l'interazione con gli utenti per muovere le tessere e risolvere il gioco !
        if (gameOver) {
          newGame();
        } else {
          // ottieni la posizione del clic
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // clic nella griglia ?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // ottieni la posizione nella griglia
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // ottieni la posizione della cella vuota
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // convertiamo in coordinate 1D
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // cerchiamo direzione per più mosse di tessere insieme
          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) {
            // muoviamo le tessere nella direzione
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // controlliamo se il gioco è risolto
          gameOver = isSolved();
        }
        
        // ripaintiamo il pannello
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // reset in stato iniziale
      shuffle(); // mescola
    } while(!isSolvable()); // fallo finché la griglia non sia risolvibile
    
    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 metà delle permutazioni del puzzle sono risolvibili.
  // Ogni volta che una tessera è preceduta da una tessera con un valore più alto conta
  // come un'inversione. Nel nostro caso, con la tessera vuota nella posizione risolta,
  // il numero di inversioni deve essere pari affinché il puzzle sia risolvibile
  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 la tessera vuota non è nella posizione risolta => non risolto
      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("Gioco del Quindici");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centra sullo schermo
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 

}

Finalmente, giochiamo!

È il momento di avviare il gioco e vederlo in azione. Il campo dovrebbe apparire come segue:

"Il gioco dei numeri" in Java - come sviluppare un gioco completo.

Proviamo a risolvere il puzzle. Se tutto è andato a buon fine, otteniamo questo:

"Il gioco dei numeri" in Java - come sviluppare un gioco completo.

Ecco fatto. Ti aspettavi qualcosa di più? 🙂

Skillbox consiglia:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster