
A Tizenöt, vagy Tizenhatos — egy nagyszerű példa egy egyszerű logikai játékra, amely világszerte népszerű. A rejtvény megoldásához a számokkal ellátott négyzeteket kell a megfelelő sorrendbe állítani, a kisebbektől a nagyobbakig. Ez nem könnyű, de izgalmas.
A mai útmutatóban bemutatjuk, hogyan fejlesszük a Tizenhatost Java 8-ban az Eclipse segítségével. A felhasználói felület fejlesztéséhez a Swing API-t használjuk.
Emlékeztetjük: minden «Habr» olvasónak 10 000 rubeles kedvezmény jár bármely Skillbox kurzus jelentkezésekor a «Habr» promóciós kód felhasználásával.
A Skillbox ajánlja: Oktatási online kurzus .
A játék tervezése
Ezen a szakaszon meg kell határozni a tulajdonságokat:
- Size — a játéktér mérete;
- nbTiles — a mezőben lévő tizenhatosok száma. nbTiles = size*size — 1;
- Tiles — a tizenhat, amely egy egyirányú tömb egész számokból áll. Minden tizenhat egyedi értéket fog kapni a [0, nbTiles] tartományban. A nulla a kihagyott négyzetet jelöli;
- blankPos — a kihagyott négyzet pozíciója.
A játék logikája
Meg kell határozni egy reset metódust, amelyet az új játékkonfiguráció inicializálására használunk. Így beállítjuk az értéket a tizenhatok tömbjének minden elemére. Ezt követően a blankPos-t a tömb utolsó pozíciójába helyezzük.
Szükség van egy shuffle metódusra is a tizenhatok tömbjének keverésére. A folyamatba nem vonjuk be a kihagyott tizenhatot, hogy az a korábbi állapotában maradhasson.
Mivel a rejtvény lehetséges kezdő pozícióinak csak a fele rendelkezik megoldással, ellenőrizni kell a kapott keverési eredményt, hogy megbizonyosodjunk arról, hogy a jelenlegi elrendezés egyáltalán megoldható. Ezt úgy tesszük, hogy meghatározzuk az isSolvable metódust.
Ha egy adott tizenhatot egy magasabb értékű tizenhat előz meg, azt inverziónak tekintjük. Amikor a kihagyott tizenhat a helyén van, az inverziók számának párosnak kell lennie ahhoz, hogy a rejtvény megoldható legyen. Így megszámoljuk az inverziók számát, és true-t adunk vissza, ha a szám páros.
Fontos meghatározni az isSolved metódust is, hogy ellenőrizzük, hogy a Game Of Fifteen elrendezésünk megoldott-e. Először nézzük, hol van a kihagyott tizenhat. Ha az eredeti pozícióban van, akkor a jelenlegi elrendezés új, korábban még nem megoldott. Ezután visszafelé végigmegyünk a csempéken, és ha a tizenhat értéke eltér a megfelelő index +1-től, false-t adunk vissza. Ellenkező esetben a metódus végén elérkezett az idő, hogy visszaadjuk a true-t, mert a rejtvény már meg van oldva.
Egy másik módszert kell meghatározni, ez pedig a newGame. Erre van szükség egy új játékpéldány létrehozásához. Ehhez visszaállítjuk a játéktáblát, majd megkeverjük, és folytatjuk, amíg a játékhelyzet megoldható nem lesz.
Itt egy példa a 15-ös logikájára vonatkozó kódra:
private void newGame() {
do {
reset(); // visszaállítás az alapállapotba
shuffle(); // keverés
} while(!isSolvable()); // addig, amíg a táblázat megoldható nem lesz
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;
}
}
// Csak a rejtvény felének permutációi megoldhatóak
// Amikor egy lapka előtt egy magasabb értékű lapka áll, az számít egy inverziónak.
// Az esetünkben, ha a fehér lapka a megoldott helyzetben van,
// akkor az inverziók számának párosnak kell lennie ahhoz, hogy a rejtvény megoldható legyen
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) // ha a fehér lapka nem a megoldott helyzetben van == nem megoldva
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Végül programozni kell a 15-ös mozgását a tömbben. Ezt a kódot később egy visszahívás (callback) révén hívjuk meg, hogy reagáljon a kurzor mozgására. A mi játékunk támogatni fogja a lapkák több mozgását egyidejűleg. Így miután átfordítottuk a képernyőn megnyomott pozíciót egy 15-ös lapkára, megkapjuk a fehér 15-ös pozícióját, és megkeressük a mozgás irányát a több mozgás támogatásához.
Itt egy példa a kódra:
// 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 fejlesztése Swing API-val
Itt az ideje foglalkozni a felülettel. Először is a Jpanel osztályt vesszük. Majd a táblára rajzoljuk a 15-ös lapkákat - minden egyes méret kiszámításához a játék konstruktorának paramétereiben megadott adatokra támaszkodunk:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;A margin is egy paraméter, amelyet a játék konstruktora határoz meg.
Most meg kell határoznunk a drawGrid metódust a hálózat és a 15-ös lapkák képernyőre rajzolásához. Elemezzük a 15-ös tömböt, és konvertáljuk a koordinátákat a felhasználói felület koordinátáira. Ezután minden egyes 15-öst a megfelelő számmal az anyag közepére rajzolunk:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// a 1D koordinátákat 2D koordinátákra alakítjuk a 2D tömb mérete alapján
int r = i / size;
int c = i % size;
// UI koordinátákra alakítjuk
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// különleges eset a üres csempe
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// más csempék esetén
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);
}
}Végül felülírjuk a paintComponent metódust, ami a JPane osztály származéka. Ezután használjuk a drawGrid metódust, majd a drawStartMessage metódust a játék indítására vonatkozó üzenet megjelenítéséhez:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Kattints az új játék indításához";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// a string s középre igazítása az adott csempe (x,y) alapján
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);
}Reagálunk a felhasználó tevékenységeire az UI-ban
Azért, hogy a játék gördülékenyen haladhasson, kezelni kell a felhasználói tevékenységeket az UI-ban. Ehhez hozzáadunk egy MouseListener implementációt a JPanel-hez, valamint a csempék mozgatásához szükséges kódot, amit már korábban bemutattunk:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// a felhasználók interakciós lehetőségei a rácson való kattintás által
// itt az ideje implementálni a felhasználói interakciót a csempék mozgatásához a játék megoldásához!
if (gameOver) {
newGame();
} else {
// a kattintás pozíciójának megkérése
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kattintás a rácson?
if (ex gridSize || ey gridSize)
return;
// pozíció a rácsban
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// az üres cella pozíciója
int c2 = blankPos % size;
int r2 = blankPos / size;
// 1D koordinátára alakítjuk
int clickPos = r1 * size + c1;
int dir = 0;
// az irány keresése több csempe egyszerre történő mozgatásához
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) {
// a csempék mozgatása az irányban
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// ellenőrizzük, hogy a játék megoldódott-e
gameOver = isSolved();
}
// újrarajzoljuk a panelt
repaint();
}
});A kódot a GameOfFifteen osztály készítőjében helyezzük el. A legvégén a newGame metódust hívjuk meg egy új játék elkezdéséhez.
A játék teljes kódja
Az utolsó lépés, mielőtt látnánk a játék működését, hogy az összes kódelem össze legyen gyűjtve. Íme, mi fog kisülni:
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;
// A 15 Puzzle játékot fogunk létrehozni Java 8 és Swing segítségével
// Ha kérdéseid vannak, nyugodtan olvasd el a megjegyzéseket ;)
public class GameOfFifteen extends JPanel { // A rácsunkat egy dedikált panelen fogjuk megrajzolni
// A 15-ös játékunk példányának mérete
private int size;
// Csempe szám
private int nbTiles;
// Rács UI dimenzió
private int dimension;
// Előtér szín
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // tetszőleges színt használunk
// Véletlenszerű objektum a csempék megkeveréséhez
private static final Random RANDOM = new Random();
// Csempék tárolása egy 1D egész számú tömbben
private int[] tiles;
// Csempe mérete a felhasználói felületen
private int tileSize;
// A üres csempe pozíciója
private int blankPos;
// Margó a rács számára a kereten
private int margin;
// Rács UI mérete
private int gridSize;
private boolean gameOver; // true, ha a játék véget ért, false egyébként
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// csempék inicializálása
nbTiles = size * size - 1; // -1 mert az üres csempét nem számítjuk
tiles = new int[size * size];
// rács méretének és csempe méretének kiszámítása
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) {
// Használják, hogy a felhasználók interakcióba lépjenek a rácsra kattintva
// itt az ideje implementálni az interakciót, hogy a csempéket mozgatni lehessen a játék megoldásához!
if (gameOver) {
newGame();
} else {
// a kattintás pozíciójának lekérdezése
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kattintás a rácsban?
if (ex gridSize || ey gridSize)
return;
// pozíció a rácsban
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// a üres cella pozíciója
int c2 = blankPos % size;
int r2 = blankPos / size;
// 1D koordinátába való konvertálás
int clickPos = r1 * size + c1;
int dir = 0;
// irány keresése több csempe mozgásához egyszerre
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) {
// csempék mozgatása a megadott irányba
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// ellenőrizzük, hogy a játék megoldódott-e
gameOver = isSolved();
}
// újrarajzoljuk a panelt
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // visszaállítás kezdeti állapotba
shuffle(); // keverés
} while(!isSolvable()); // addig, amíg megoldható nem lesz a rács
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;
}
}
// Csak a puzzle felének permutációi oldhatók meg.
// Amikor egy csempe egy nagyobb értékű csempe előtt áll, az számít
// mint egy inverzió. A mi esetünkben, az üres csempe megoldott állapotban,
// a fordítottak számának párosnak kell lennie a puzzle megoldásához
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) // ha az üres csempe nem a megoldott állapotban van => nem megoldott
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("Tizenötödik Játék");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// középre igazítani a képernyőn
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Végre játszunk!
Itt az ideje, hogy elindítsuk a játékot és ellenőrizzük, hogyan működik. A térképnek a következőképpen kell kinéznie:

Próbálkozunk a rejtvény megoldásával. Ha minden rendben ment, ezt kapjuk:

Ennyi volt. Többre számítottál? 🙂
A Skillbox ajánlja:
- Gyakorlati kurzus .
- Alkalmazott online kurzus .
- Kétéves gyakorlati tanfolyam .
Forrás: habr.com
