
â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 .
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:

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

Das ist alles. HĂ€tten Sie mehr erwartet? đ
Skillbox empfiehlt:
- Praktischer Kurs .
- Angewandter Online-Kurs .
- ZweijÀhriger praktischer Kurs .
Quelle: habr.com
