
»Petnajst«, oziroma »Petnajška« — odličen primer preproste logične igre, ki je priljubljena po vsem svetu. Za rešitev uganke je potrebno razporediti kvadratke s številkami v pravem vrstnem redu, od najmanjšega do največjega. Nije lahko, a zanimivo.
V današnjem tutorialu prikazujemo, kako razviti »Petnajst« na Javi 8 z uporabo Eclipse. Za razvoj uporabniškega vmesnika bomo uporabili Swing API.
Opominjamo: za vse bralce »Habra« — popust 10 000 rubljev ob vpisu na kateri koli tečaj Skillbox s promo kodo »Habr«.
Skillbox priporoča: Образовательный онлайн-курс .
Oblikovanje igre
V tej fazi je treba določiti lastnosti:
- Size — velikost igralne površine;
- nbTiles — število petnajstk v polju. nbTiles = size*size — 1;
- Tiles — petnajstka, ki predstavlja enodimenzionalno tabelo celih števil. Vsaka od petnajstk bo dobila edinstveno vrednost v razponu [0, nbTiles]. Ničla označuje prazni kvadrat;
- blankPos — položaj praznega kvadrata.
Logika igre
Treba je določiti metodo reset, ki se uporablja za inicializacijo novega igralnega položaja. Tako nastavimo vrednost za vsak element tabele petnajstk. Nato prazno petnajstko postavimo na zadnji položaj tabele.
Potrebna je tudi metoda shuffle za premešanje tabele petnajstk. Ne vključujemo prazne petnajstke v proces premeščanja, da jo obdržimo na prejšnjem mestu.
Ker ima le polovica možnih začetnih položajev uganke rešitev, je potrebno preveriti nastali rezultat premešanja, da se prepričamo, da je trenutni položaj sploh rešljiv. Da bi to naredili, določimo metodo isSolvable.
Če določeni petnajstki prehaja petnajstka z višjo vrednostjo, to velja za obrat. Ko je prazna petnajstka na svojem mestu, mora biti število obratov sodo, da je uganka rešena. Tako preštejemo število obratov in vrnemo true, če je število sodo.
Nato je pomembno določiti metodo isSolved, da preverimo, ali je naš položaj igre Game Of Fifteen rešen. Najprej si ogledamo, kje se nahaja prazna petnajstka. Če je na začetni poziciji, je trenutni položaj nov, pred tem še nerešen. Nato pregledujemo ploščice v obratnem vrstnem redu, in če se vrednost petnajstke razlikuje od ustreznega indeksa +1, vrnemo false. V nasprotnem primeru pa je na koncu metode čas, da vrnemo true, ker je uganka že rešena.
Drugi način, ki ga je treba določiti, je newGame. Potreben je za ustvarjanje novega izvodu igre. Za to ponastavimo igralno polje, ga premešamo in nadaljujemo, dokler igralna pozicija ni rešena.
Tukaj je primer kode s ključnimi logikami petice:
private void newGame() {
do {
reset(); // ponastavi v začetno stanje
shuffle(); // premešaj
} while(!isSolvable()); // nadaljuj, dokler mreža ne postane rešena
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;
}
}
// Le polovica permutacij uganke je rešljiva/
// Kadar koli je plošček pred ploščkom z višjo vrednostjo, se šteje
// kot obrat. V našem primeru, s praznim ploščkom na rešenem mestu,
// mora biti število obratov sode, da je uganka rešena.
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) // če prazni plošček ni na rešenem mestu == ne rešen
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Nazadnje je treba programirati premikanje ploščkov v polju. Ta koda se bo pozneje klicala preko povratnega klica, da bi se odzvala na gibanje kurzorja. Naša igra bo podpirala več premikov ploščkov hkrati. Ko prenesemo pritisnjeno pozicijo na zaslonu na plošček, dobimo pozicijo praznega ploščka in poiščemo smer gibanja za podporo več premikom hkrati.
Tukaj je primer kode:
// 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;Razvijamo UI na Swing API
Zdaj je čas, da se posvetimo vmesniku. Najprej vzamemo razred Jpanel. Nato na polju narišemo ploščke – za izračun velikosti vsakega uporabimo podatke, določene v parametru konstruktorja igre:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin je prav tako parameter, določen v konstruktorju igre.
Zdaj je treba določiti metodo drawGrid za risanje mreže in ploščkov na zaslonu. Analiziramo polje ploščkov in pretvorimo koordinate v koordinate uporabniškega vmesnika. Nato narišemo vsak plošček s pripadajočim številko v središču:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// we convert 1D coords to 2D coords given the size of the 2D Array
int r = i / size;
int c = i % size;
// we convert in coords on the UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// check special case for blank tile
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// for other tiles
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);
}
}Najprej bomo nadomestili metodo paintComponent, ki je izpeljana iz razreda JPane. Potem bomo uporabili metodo drawGrid in nato metodo drawStartMessage za prikaz sporočila, ki spodbuja klik, da začnete igro:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Kliknite za začetek nove igre";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// center string s for the given tile (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);
}Reagiramo na dejanja uporabnika v uporabniškem vmesniku.
Da igra poteka gladko, je potrebno obravnavati dejanja uporabnika v uporabniškem vmesniku. Za to dodamo implementacijo MouseListener na Jpanel in kodo za premikanje ploščic, ki je bila že prikazana zgoraj:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// used to let users to interact on the grid by clicking
// it's time to implement interaction with users to move tiles to solve the game !
if (gameOver) {
newGame();
} else {
// get position of the click
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// click in the grid ?
if (ex gridSize || 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;
}
// we check if game is solved
gameOver = isSolved();
}
// we repaint panel
repaint();
}
});Koda umestimo v konstruktor razreda GameOfFifteen. Na koncu pokličemo metodo newGame za začetek nove igre.
Poln koda igre
Zadnji korak, preden vidimo igro v akciji, je, da združimo vse elemente kode. Tukaj je, kaj dobimo:
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;
// Создаём игру Пятнашки на Java 8 и Swing
// Если у вас есть вопросы, не стесняйтесь читать комментарии ;)
public class GameOfFifteen extends JPanel { // наша сетка будет отображаться в отдельной панели
// Размер нашего экземпляра Пятнашек
private int size;
// Количество плиток
private int nbTiles;
// Размеры графического интерфейса сетки
private int dimension;
// Цвет переднего плана
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // используем произвольный цвет
// Объект Random для перемешивания плиток
private static final Random RANDOM = new Random();
// Храним плитки в одномерном массиве целых чисел
private int[] tiles;
// Размер плитки на графическом интерфейсе
private int tileSize;
// Позиция пустой плитки
private int blankPos;
// Отступ для сетки на фрейме
private int margin;
// Размер сетки интерфейса
private int gridSize;
private boolean gameOver; // true, если игра окончена, false — в противном случае
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// Инициализация плиток
nbTiles = size * size - 1; // - 1, потому что не учитываем пустую плитку
tiles = new int[size * size];
// Расчёт размеров сетки и плитки
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) {
// позволяет пользователям взаимодействовать с сеткой, нажимая на неё
// настало время реализовать взаимодействие с пользователями для перемещения плиток и решения игры!
if (gameOver) {
newGame();
} else {
// получаем позицию клика
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// кликнули в сетке?
if (ex gridSize || ey gridSize)
return;
// получаем позицию в сетке
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// получаем позицию пустой клетки
int c2 = blankPos % size;
int r2 = blankPos / size;
// переводим в одномерные координаты
int clickPos = r1 * size + c1;
int dir = 0;
// ищем направление для множественных перемещений плиток одновременно
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) {
// перемещаем плитки в указанном направлении
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// проверяем, решена ли игра
gameOver = isSolved();
}
// перерисовываем панель
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // сброс в начальное состояние
shuffle(); // перемешивание
} while(!isSolvable()); // продолжаем, пока сетка не станет решаемой
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;
}
}
// Только половина перестановок головоломки являются решаемыми.
// Каждый раз, когда плитка предшествует плитке с большим значением, это считается
// как инверсия. В нашем случае, при пустой плитке в решённом положении,
// количество инверсий должно быть чётным, чтобы головоломка была решаемой
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) // если пустая плитка не в решённом положении == не решена
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("Игра в Пятнашки");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// центрируем на экране
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Nazad, igrajmo!
Pravi trenutak je da pokrenemo igru i proverimo je u akciji. Polje bi trebalo da izgleda ovako:

Pokušavamo da rešimo zagonetku. Ako sve prođe uspešno, dobijamo ovo:

To je to. Da li ste očekivali više? 🙂
Skillbox priporoča:
- Praktični tečaj .
- Praktični spletni tečaj .
- Двухлетний практический курс .
Izvor: habr.com
