„Die 15“ in Java – wie man ein vollwertiges Spiel entwickelt

„Die 15“ in Java – wie man ein vollwertiges Spiel entwickelt

„FĂŒnfzehn“, oder „FĂŒnfzehner“ — ist ein hervorragendes Beispiel fĂŒr ein einfaches logisches Spiel, das auf der ganzen Welt beliebt ist. Um das RĂ€tsel zu lösen, mĂŒssen die Quadrate mit den Zahlen der Reihe nach angeordnet werden, von der kleineren zur grĂ¶ĂŸeren. Das ist nicht einfach, aber interessant.

In diesem Tutorial zeigen wir, wie man „FĂŒnfzehn“ in Java 8 mit Eclipse entwickelt. FĂŒr die UI-Entwicklung verwenden wir die 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-Kurs „Beruf Java-Entwickler“.

Spieldesign

In dieser Phase mĂŒssen die Eigenschaften festgelegt werden:

  • Size — GrĂ¶ĂŸe des Spielbretts;
  • nbTiles — Anzahl der FĂŒnfzehner auf dem Brett. nbTiles = size*size — 1;
  • Tiles — FĂŒnfzehner, die als eindimensionales Array von Ganzzahlen dargestellt werden. Jeder der FĂŒnfzehner erhĂ€lt einen einzigartigen Wert im Bereich [0, nbTiles]. Null bezeichnet das leere Feld;
  • blankPos — Position des leeren Feldes.

Spiel-Logik

Es muss eine Reset-Methode definiert werden, die zur Initialisierung einer neuen Spielsituation verwendet wird. Dadurch setzen wir den Wert fĂŒr jedes Element im Array der FĂŒnfzehner. Dann platzieren wir blankPos an der letzten Position des Arrays.

Außerdem wird eine Shuffle-Methode benötigt, um das Array der FĂŒnfzehner zu mischen. Wir schließen den leeren FĂŒnfzehner vom Mischvorgang aus, um ihn in seiner aktuellen Position zu belassen.

Da nur die HĂ€lfte der möglichen Startpositionen des RĂ€tsels lösbar ist, muss das resultierende Mischen ĂŒberprĂŒft werden, um sicherzustellen, dass die aktuelle Anordnung ĂŒberhaupt lösbar ist. Dazu definieren wir die Methode isSolvable.

Wenn einem bestimmten FĂŒnfzehner ein FĂŒnfzehner mit einem höheren Wert vorausgeht, wird dies als Inversion angesehen. Wenn der leere FĂŒnfzehner an seiner Position ist, muss die Anzahl der Inversionen gerade sein, damit das RĂ€tsel lösbar ist. Also zĂ€hlen wir die Inversionen und geben true zurĂŒck, wenn die Zahl gerade ist.

Es ist dann wichtig, die Methode isSolved zu definieren, um zu ĂŒberprĂŒfen, ob unsere Anordnung Game Of Fifteen gelöst ist. Zuerst schauen wir, wo sich der leere FĂŒnfzehner befindet. Wenn er an der Anfangsposition ist, ist die aktuelle Anordnung neu und wurde noch nie gelöst. Dann durchlaufen wir die Spielsteine in umgekehrter Reihenfolge, und wenn der Wert des FĂŒnfzehners vom entsprechenden Index +1 abweicht, geben wir false zurĂŒck. Andernfalls ist es am Ende der Methode an der Zeit, true zurĂŒckzugeben, denn das RĂ€tsel ist bereits gelöst.

Ein weiterer zu definierender Methode ist newGame. Sie ist notwendig, um eine neue Instanz des Spiels zu erstellen. DafĂŒr setzen wir das Spielfeld zurĂŒck, mischen es und fahren fort, bis die Spielsituation lösbar ist.

Hier ist ein Beispielcode mit der SchlĂŒssel-Logik der 15er-Puzzle:

private void newGame() {
  do {
    reset(); // zurĂŒcksetzen in den ursprĂŒnglichen Zustand
    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 Fliese von einer Fliese mit höherem Wert gefolgt wird, zÀhlt es
// als eine Inversion. In unserem Fall, mit der leeren Fliese in der gelösten Position,
// 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 => nicht gelöst
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Schließlich mĂŒssen wir die Bewegung der 15er-Puzzle im Array programmieren. Dieser Code wird spĂ€ter ĂŒber einen Callback aufgerufen, um auf die Bewegung des Cursors zu reagieren. Unser Spiel wird mehrere gleichzeitige Bewegungen der Fliesen unterstĂŒtzen. Nachdem wir die gedrĂŒckte Position auf dem Bildschirm in die 15er-Puzzle umgewandelt haben, ermitteln wir die Position der leeren Fliese und suchen die Bewegungsrichtung, um mehrere gleichzeitige Bewegungen zu unterstĂŒtzen.

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;

UI-Entwicklung mit der Swing-API

Jetzt ist es an der Zeit, uns um das Interface zu kĂŒmmern. Zuerst nehmen wir die Klasse JPanel. Dann zeichnen wir die Fliesen auf dem Feld — zur Berechnung der GrĂ¶ĂŸe jeder Fliese nutzen wir die im Konstruktor des Spiels festgelegten Daten:

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

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

Jetzt mĂŒssen wir die Methode drawGrid definieren, um das Raster und die Fliesen auf dem Bildschirm zu zeichnen. Wir analysieren das Array der Fliesen und konvertieren die Koordinaten in die Koordinaten der BenutzeroberflĂ€che. Dann zeichnen wir jede Fliese 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 BenutzeroberflÀche
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;
 
    // prĂŒfen auf den Sonderfall des leeren Feldes
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }
 
      continue;
    }
 
    // fĂŒr andere Felder
    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);
  }
}

Schließlich ĂŒberschreiben wir die Methode paintComponent, die von der Klasse JPane abgeleitet ist. Danach verwenden wir die Methode drawGrid und schließlich die Methode drawStartMessage, um die 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, 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) {
  // zentriere den String s fĂŒr das gegebene 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 die Aktionen des Benutzers in der BenutzeroberflÀche.

Damit das Spiel reibungslos ablĂ€uft, mĂŒssen die Benutzeraktionen in der BenutzeroberflĂ€che verarbeitet werden. Dazu fĂŒgen wir die Implementierung von MouseListener auf Jpanel hinzu und den Code zur Bewegung der Kacheln, der bereits oben gezeigt wurde:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // verwendet, um Benutzern Interaktionen auf dem Gitter durch Klicken zu ermöglichen
    // es ist an der Zeit, die Interaktion mit Benutzern zu implementieren, um die Kacheln zu bewegen und das Spiel zu lösen!
    if (gameOver) {
      newGame();
    } else {
      // position des Klicks ermitteln
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;
 
      // Klick im Gitter?
      if (ex  gridSize || ey  gridSize)
        return;
 
      // Position im Gitter ermitteln
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;
 
      // Position der leeren Zelle ermitteln
      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 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 Kacheln 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();
  }
});

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

VollstÀndiger Spielcode

Der letzte Schritt, bevor wir das Spiel in Aktion sehen, besteht darin, alle Codeelemente zusammenzubringen. Das wird passieren:

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
// Bei Fragen, zögere nicht, die Kommentare zu lesen ;)
public class GameOfFifteen extends JPanel { // unser Raster wird in einem speziellen Panel gezeichnet
 
  // GrĂ¶ĂŸe unserer Game of Fifteen Instanz
  private int size;
  // Anzahl der Kacheln
  private int nbTiles;
  // Raster UI Dimension
  private int dimension;
  // Vordergrundfarbe
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // wir verwenden eine beliebige Farbe
  // Zufallsobjekt zum Mischen der Kacheln
  private static final Random RANDOM = new Random();
  // Speicherung der Kacheln in einem 1D Array von ganzen Zahlen
  private int[] tiles;
  // GrĂ¶ĂŸe der Kachel auf der BenutzeroberflĂ€che
  private int tileSize;
  // Position der leeren Kachel
  private int blankPos;
  // Rand fĂŒr das Raster im Fenster
  private int margin;
  // Raster UI GrĂ¶ĂŸe
  private int gridSize;
  private boolean gameOver; // true, wenn das Spiel vorbei ist, false andernfalls
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // initialisiere Kacheln
    nbTiles = size * size - 1; // -1, weil wir die leere Kachel nicht zÀhlen
    tiles = new int[size * size];
    
    // berechne RastergrĂ¶ĂŸe und KachelgrĂ¶ĂŸ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, um Benutzern zu ermöglichen, mit dem Raster zu interagieren, indem sie klicken
        // es ist Zeit, die Interaktion mit Benutzern zu implementieren, um Kacheln zu bewegen, um das Spiel zu lösen!
        if (gameOver) {
          newGame();
        } else {
          // hole Position des Klicks
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // Klick im Raster ?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // hole Position im Raster
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // hole Position der leeren Zelle
          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 Kachelbewegungen 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 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 malen das Panel neu
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // zurĂŒcksetzen in den ursprĂŒnglichen Zustand
      shuffle(); // mischen
    } while(!isSolvable()); // mache es, 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.
  // Jedes Mal, wenn eine Kachel von einer Kachel mit höherem Wert vorangegangen wird, zÀhlt es
  // 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 Kachel nicht in der gelösten Position ist ==> nicht gelöst
      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 der FĂŒnfzehn");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // zentriere auf dem Bildschirm
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Endlich spielen wir!

Jetzt ist es an der Zeit, das Spiel zu starten und es in Aktion zu testen. Das Feld sollte folgendermaßen aussehen:

„Die 15“ in Java – wie man ein vollwertiges Spiel entwickelt

Lassen Sie uns versuchen, das RÀtsel zu lösen. Wenn alles erfolgreich war, erhalten wir Folgendes:

„Die 15“ in Java – wie man ein vollwertiges Spiel entwickelt

Das ist alles. Hatten Sie mehr erwartet? 🙂

Skillbox empfiehlt:

Quelle: habr.com

60GB SSD 8Gb DDR4