Das '15-Puzzle' in Java – so entwickeln Sie ein vollwertiges Spiel

Das '15-Puzzle' in Java – so entwickeln Sie ein vollwertiges Spiel

„FĂŒnfzehn“, oder „FĂŒnfzehner“ ist ein hervorragendes Beispiel fĂŒr ein einfaches Logikspiel, das weltweit beliebt ist. Um das RĂ€tsel zu lösen, mĂŒssen die PlĂ€ttchen mit den Zahlen in der richtigen Reihenfolge von klein nach groß angeordnet werden. Das ist nicht einfach, aber spannend.

In diesem Tutorial zeigen wir, wie man „FĂŒnfzehn“ mit Java 8 und Eclipse entwickelt. FĂŒr die UI-Entwicklung verwenden wir das Swing API.

Wir erinnern daran: alle Leser von „Habr“ erhalten einen Rabatt von 10.000 Rubel bei der Anmeldung zu einem beliebigen Kurs von Skillbox mit dem Aktionscode „Habr“.

Skillbox empfiehlt: Online-Bildungskurs Beruf: Java-Entwickler.

Spielentwurf

In diesem Schritt mĂŒssen die Eigenschaften festgelegt werden:

  • Size — GrĂ¶ĂŸe des Spielfelds;
  • nbTiles — Anzahl der PlĂ€ttchen im Feld. nbTiles = size * size — 1;
  • Tiles — die fĂŒnfzehn, die als eindimensionales Array von Ganzzahlen dargestellt wird. Jedes PlĂ€ttchen erhĂ€lt einen einzigartigen Wert im Bereich [0, nbTiles]. Null steht fĂŒr das leere Feld;
  • blankPos — Position des leeren Feldes.

Spiel-Logik

Es muss eine Reset-Methode definiert werden, die verwendet wird, um eine neue Spielsituation zu initialisieren. Damit setzen wir den Wert fĂŒr jedes Element des PlĂ€ttchen-Arrays. Anschließend platzieren wir blankPos an der letzten Position des Arrays.

Außerdem benötigen wir eine Shuffle-Methode, um das PlĂ€ttchen-Array zu mischen. Wir schließen das leere PlĂ€ttchen von diesem Prozess aus, um es in seiner ursprĂŒnglichen Position zu belassen.

Da nur die HĂ€lfte der möglichen Startpositionen des RĂ€tsels lösbar ist, muss das Ergebnis der Mischung ĂŒberprĂŒft werden, um sicherzustellen, dass die aktuelle Konfiguration tatsĂ€chlich lösbar ist. Dazu definieren wir die Methode isSolvable.

Wenn eine bestimmte FĂŒnfzehn vor einer FĂŒnfzehn mit einem höheren Wert steht, gilt dies als Inversion. Wenn das leere Feld an seiner Stelle ist, muss die Anzahl der Inversionen gerade sein, damit das RĂ€tsel lösbar ist. Daher zĂ€hlen wir die Inversionen und geben true zurĂŒck, wenn die Anzahl gerade ist.

Es ist dann wichtig, die Methode isSolved zu bestimmen, um zu ĂŒberprĂŒfen, ob unser Layout von Game Of Fifteen gelöst ist. Zuerst schauen wir, wo sich das leere Feld befindet. Wenn es in der ursprĂŒnglichen Position ist, ist die aktuelle Anordnung neu und wurde zuvor nicht gelöst. Anschließend durchlaufen wir die Kacheln in umgekehrter Reihenfolge, und wenn der Wert der Kachel von dem entsprechenden Index +1 abweicht, geben wir false zurĂŒck. Andernfalls ist es am Ende der Methode Zeit, true zurĂŒckzugeben, weil das RĂ€tsel bereits gelöst ist.

Eine weitere Methode, die definiert werden muss, ist newGame. Sie ist erforderlich, um eine neue Instanz des Spiels zu erstellen. Dazu setzen wir das Spielfeld zurĂŒck, mischen es und wiederholen dies, bis die Spielposition lösbar ist.

Hier ist ein Codebeispiel mit der SchlĂŒssel-Logik des Spiels:

private void newGame() {
  do {
    reset(); // zurĂŒcksetzen in den Ausgangszustand
    shuffle(); // mischen
  } while(!isSolvable()); // solange machen, bis das Raster lösbar ist
 
  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;
  }
}
 
// Nur die HÀlfte der Permutationen des Puzzles sind lösbar/
// Wann immer eine Kachel von einer Kachel mit höherem Wert vorangegangen wird, zÀhlt das
// als Inversion. In unserem Fall muss, mit der leeren Kachel in der gelösten Position,
// die Anzahl der Inversionen gerade sein, damit das Puzzle lösbar ist
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) // wenn die leere Kachel nicht in der gelösten Position ist ==>
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Zu guter Letzt mĂŒssen wir die Bewegung der Kacheln im Array programmieren. Dieser Code wird spĂ€ter ĂŒber einen Callback aufgerufen, um auf die Mauszeigerbewegung zu reagieren. Unser Spiel wird mehrere Kachelbewegungen gleichzeitig unterstĂŒtzen. Nachdem wir die angeklickte Position auf dem Bildschirm in eine Kachel umgewandelt haben, erhalten wir die Position der leeren Kachel und suchen nach der Bewegungsrichtung, um mehrere Bewegungen gleichzeitig zu ermöglichen.

Hier ist ein Beispielcode:

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

Wir entwickeln das UI mit der Swing API

Es ist an der Zeit, sich mit dem Interface zu befassen. Zuerst verwenden wir die Klasse JPanel. Dann zeichnen wir die Kacheln auf dem Feld – fĂŒr die Berechnung der GrĂ¶ĂŸe jeder Kachel nutzen wir die im Konstruktor des Spiels angegebenen Daten:

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

Der Abstand (Margin) ist ebenfalls ein Parameter, der im Konstruktor des Spiels festgelegt wird.

Jetzt mĂŒssen wir die Methode drawGrid definieren, um das Gitter und die Kacheln auf dem Bildschirm zu zeichnen. Wir analysieren das Array der Kacheln und konvertieren die Koordinaten in UI-Koordinaten. Dann zeichnen wir jede Kachel mit der entsprechenden Nummer in der Mitte:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // wir konvertieren 1D-Koordinaten in 2D-Koordinaten, basierend auf der GrĂ¶ĂŸe des 2D-Arrays
    int r = i / size;
    int c = i % size;
    // wir konvertieren in Koordinaten auf der UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;
 
    // ÜberprĂŒfen des Spezialfalls fĂŒr leere Kacheln
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }
 
      continue;
    }
 
    // fĂŒr andere Kacheln
    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);
  }
}

Letztendlich ĂŒberschreiben wir die Methode paintComponent, die von der Klasse JPane abgeleitet ist. Dann verwenden wir die Methode drawGrid und anschließend die Methode drawStartMessage, um eine Nachricht anzuzeigen, die zum Klicken einlĂ€dt, um das Spiel zu starten:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Klicken Sie hier, um ein neues Spiel zu starten";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}
 
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // zentrieren Sie den String s fĂŒr das angegebene Feld (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);
}

Wir reagieren auf Benutzeraktionen in der BenutzeroberflÀche

Damit das Spiel reibungslos lĂ€uft, mĂŒssen Benutzeraktionen in der BenutzeroberflĂ€che verarbeitet werden. Dazu fĂŒgen wir eine Implementierung von MouseListener zu JPanel hinzu und verwenden den oben bereits gezeigten Code fĂŒr die Bewegung der Kacheln:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // verwendet, damit Benutzer durch Klicken mit dem Gitter interagieren können
    // es ist an der Zeit, die Interaktion mit den Benutzern zu implementieren, um die Kacheln zur Lösung des Spiels zu bewegen!
    if (gameOver) {
      newGame();
    } else {
      // erhalte die Position des Klicks
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // Klick im Gitter?
      if (ex  gridSize  || ey  gridSize)
        return;

      // erhalte die Position im Gitter
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // erhalte die Position der leeren Zelle
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // wir konvertieren in die 1D-Koordinate
      int clickPos = r1 * size + c1;

      int dir = 0;

      // wir suchen die Richtung fĂŒr mehrere Kachelbewegungen gleichzeitig
      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) {
        // wir bewegen die Kacheln in die Richtung
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // wir ĂŒberprĂŒfen, ob das Spiel gelöst ist
      gameOver = isSolved();
    }

    // wir zeichnen das Panel neu
    repaint();
  }
});

Der Code wird im Konstruktor der Klasse GameOfFifteen platziert. Am Ende rufen wir die Methode newGame auf, um ein neues Spiel zu starten.

VollstÀndiger Code des Spiels

Der letzte Schritt, bevor wir das Spiel in Aktion sehen, besteht darin, alle Codeelemente zusammenzustellen. So könnte es aussehen:

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;
 
// Wir werden ein Spiel von 15 Puzzle mit Java 8 und Swing erstellen
// Wenn Sie Fragen haben, können Sie die Kommentare lesen ;)
public class GameOfFifteen extends JPanel { // unser Raster wird in einem eigenen Panel gezeichnet
 
  // GrĂ¶ĂŸe unserer Game of Fifteen Instanz
  private int size;
  // Anzahl der Fliesen
  private int nbTiles;
  // UI Dimension des Rasters
  private int dimension;
  // Vordergrundfarbe
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // wir verwenden eine beliebige Farbe
  // Zufallsobjekt zum Mischen der Fliesen
  private static final Random RANDOM = new Random();
  // Speicherung der Fliesen in einem 1D-Array von Ganzzahlen
  private int[] tiles;
  // GrĂ¶ĂŸe der Fliese im UI
  private int tileSize;
  // Position der leeren Fliese
  private int blankPos;
  // Rand fĂŒr das Raster auf dem Rahmen
  private int margin;
  // UI GrĂ¶ĂŸe des Rasters
  private int gridSize;
  private boolean gameOver; // true, wenn das Spiel zu Ende ist, andernfalls false
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // initialisieren der Fliesen
    nbTiles = size * size - 1; // -1, weil wir die leere Fliese nicht zÀhlen
    tiles = new int[size * size];
    
    // Berechnung der Raster- und FliesengrĂ¶ĂŸe
    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) {
        // wird verwendet, damit Benutzer mit dem Raster interagieren können, indem sie klicken
        // es ist Zeit, die Interaktion mit den Benutzern zu implementieren, um die Fliesen zu bewegen und das Spiel zu lösen!
        if (gameOver) {
          newGame();
        } else {
          // Position des Klicks abrufen
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // Klick im Raster?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // Position im Raster abrufen
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // Position der leeren Zelle abrufen
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // wir konvertieren in die 1D-Koordinaten
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // wir suchen die Richtung fĂŒr mehrere Fliesenbewegungen auf einmal
          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) {
            // wir bewegen die Fliesen in die Richtung
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // wir prĂŒfen, ob das Spiel gelöst ist
          gameOver = isSolved();
        }
        
        // wir malen das Panel neu
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // zurĂŒcksetzen in den ursprĂŒnglichen Zustand
      shuffle(); // mischen
    } while(!isSolvable()); // machen Sie es so lange, bis das Raster lösbar ist
    
    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;
    }
  }
 
  // Nur die HÀlfte der Permutationen des Puzzles sind lösbar.
  // Immer wenn eine Fliese von einer Fliese mit einem höheren Wert vorausgeht, zÀhlt das
  // als eine Inversion. In unserem Fall muss die Anzahl der Inversionen gerade sein,
  // damit das Puzzle lösbar ist
  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) // wenn die leere Fliese nicht in der gelösten Position ist ==>
      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("Spiel von Fifteen");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // zentrieren auf dem Bildschirm
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Endlich geht's los!

Es ist an der Zeit, das Spiel zu starten und es in Aktion zu prĂŒfen. Das Spielfeld sollte folgendermaßen aussehen:

Das '15-Puzzle' in Java – so entwickeln Sie ein vollwertiges Spiel

Lass uns das RÀtsel lösen. Wenn alles gut lÀuft, erhalten wir Folgendes:

Das '15-Puzzle' in Java – so entwickeln Sie ein vollwertiges Spiel

Das ist alles. HĂ€tten Sie mehr erwartet? 🙂

Skillbox empfiehlt:

Quelle: habr.com

Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster