
«Petnaest», ili «Petnaestica» — izvrstan primjer jednostavne logičke igre, popularne diljem svijeta. Da biste riješili zagonetku, potrebno je posložiti kvadrate s brojevima prema redu, od manjeg prema većem. To nije lako, ali je zanimljivo.
U današnjem tutorialu pokazujemo kako razviti «Petnaesticu» u Javi 8 koristeći Eclipse. Za razvoj UI-a koristit ćemo Swing API.
Podsjećamo: za sve čitatelje "Habra" — popust od 10 000 rubalja prilikom upisa na bilo koji tečaj Skillbox uz promo kod "Habr".
Skillbox preporučuje: Online edukacijski tečaj .
Projektiranje igre
U ovoj fazi trebate definirati svojstva:
- Size — veličina igrališta;
- nbTiles — broj pločica na polju. nbTiles = size*size — 1;
- Tiles — pločica, koja predstavlja jednodimenzionalni niz cijelih brojeva. Svaka od pločica će dobiti jedinstvenu vrijednost u rasponu [0, nbTiles]. Nula označava prazni kvadrat;
- blankPos — pozicija praznog kvadrata.
Logika igre
Trebate definirati metodu reset, koja se koristi za inicijalizaciju nove pozicije igre. Tako postavljamo vrijednost za svaki element niza pločica. A zatim stavljamo blankPos u posljednju poziciju niza.
Također je potrebna metoda shuffle za miješanje niza pločica. Ne uključujemo praznu pločicu u proces miješanja, kako bi ostala na istom mjestu.
Budući da samo polovica mogućih početnih pozicija zagonetke ima rješenje, potrebno je provjeriti rezultat miješanja kako bismo se uvjerili da je trenutna postavka uopće rješiva. Da bismo to učinili, definiramo metodu isSolvable.
Ako specifikanoj pločici prethodi pločica s višom vrijednošću, to se smatra inverzijom. Kada je prazna pločica na svom mjestu, broj inverzija mora biti paran kako bi zagonetka bila rješiva. Tako prebrojavamo broj inverzija i vraćamo true, ako je broj paran.
Zatim je važno definirati metodu isSolved, kako bismo provjerili je li naš raspored Game Of Fifteen riješen. Prvo gledamo gdje se nalazi prazna pločica. Ako je na početnoj poziciji, tada je trenutni raspored nov, nikada ranije nije bio riješen. Zatim prolazimo kroz pločice unatrag, i ako se vrijednost pločice razlikuje od odgovarajućeg indeksa +1, vraćamo false. U suprotnom, na kraju metode vrijeme je da vratimo true, jer je zagonetka već riješena.
Još jedna metoda koju treba definirati je newGame. Ona je potrebna za stvaranje nove instance igre. Za to ćemo resetirati polje igre, zatim ga promiješati i nastaviti sve dok pozicija igre ne bude rješiva.
Evo primjera koda s ključnom logikom za 15-puzzle:
private void newGame() {
do {
reset(); // resetiraj na početno stanje
shuffle(); // promiješaj
} while(!isSolvable()); // ponavljaj dok mreža ne postane rješiva
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;
}
}
// Samo polovica permutacija slagalice je rješiva
// Kad god je pločica prethodila pločici s višom vrijednošću, to se broji
// kao obrnuta situacija. U našem slučaju, s praznom pločicom u rješenom položaju,
// broj obrnutih situacija mora biti paran da bi slagalica bila rješiva
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) // ako prazna pločica nije na rješenom položaju ==> nije rješena
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Napokon, potrebno je programirati kretanje pločica u nizu. Ovaj kod će se kasnije pozivati putem povratne funkcije (callback), kako bi reagirao na kretanje pokazivača. Naša igra će podržavati više pomicanja pločica istovremeno. Nakon što pretvorimo pritisnutu poziciju na ekranu u 15-puzzle, dobivamo poziciju prazne pločice i tražimo smjer kretanja za podršku višestrukim pomicanjima.
Evo primjera koda:
// 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 koristeći Swing API
Vrijeme je za rad na sučelju. Prvo uzimamo klasu Jpanel. Zatim na polju crtamo pločice — za izračun veličine svake koristićemo podatke definirane u parametru konstruktora igre:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin je također parametar definiran u konstruktoru igre.
Sada trebamo definirati metodu drawGrid za crtanje mreže i pločica na ekranu. Analiziramo niz pločica i pretvaramo koordinate u koordinate korisničkog sučelja. Zatim ćemo nacrtati svaku pločicu sa odgovarajućim brojem u središtu:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// pretvaramo 1D koordinate u 2D koordinate na temelju veličine 2D niza
int r = i / size;
int c = i % size;
// pretvaramo u koordinate na UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// provjeravamo posebni slučaj za praznu pločicu
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// za ostale pločice
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);
}
}Na kraju, overridden metode paintComponent, koja je izvedena iz klase JPane. Zatim koristimo metodu drawGrid, a zatim metodu drawStartMessage za prikaz poruke koja predlaže klik na pokretanje igre:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Kliknite za pokretanje nove igre";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// centriraj string s za danu pločicu (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 korisničke akcije u UI
Kako bi igra napredovala, potrebno je obraditi korisničke akcije u UI. Za to dodajemo implementaciju MouseListener na Jpanel i kod za pomicanje pločica, već prikazanog ranije:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// korišteno da korisnicima omogući interakciju na mreži klikom
// vrijeme je za implementaciju interakcije s korisnicima za pomicanje pločica kako bi se riješila igra!
if (gameOver) {
newGame();
} else {
// dobijemo poziciju klika
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klik u mreži?
if (ex gridSize || ey gridSize)
return;
// dobijemo poziciju u mreži
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// dobijemo poziciju prazne ćelije
int c2 = blankPos % size;
int r2 = blankPos / size;
// pretvaramo u 1D koordinatu
int clickPos = r1 * size + c1;
int dir = 0;
// tražimo smjer za više pomaka pločica odjednom
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) {
// pomičemo pločice u smjeru
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// provjeravamo je li igra riješena
gameOver = isSolved();
}
// ponovno slikamo panel
repaint();
}
});Kod stavljamo u konstruktor klase GameOfFifteen. Na kraju pozivamo metodu newGame za započinjanje nove igre.
Cijeli kod igre
Posljednji korak prije nego što vidimo igru u akciji je sastaviti sve elemente koda zajedno. Evo što će proizaći:
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;
// Kreiramo igru Puzzle od 15 s pomoću Java 8 i Swing
// Ako imate pitanja, slobodno pročitajte komentare ;)
public class GameOfFifteen extends JPanel { // Naša mreža će biti nacrtana u posebnoj Paneli
// Veličina naše instance igre Puzzle od 15
private int size;
// Broj pločica
private int nbTiles;
// Dimenzije UI mreže
private int dimension;
// Boja prednjeg plana
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // koristimo proizvoljnu boju
// Random objekt za miješanje pločica
private static final Random RANDOM = new Random();
// Pohranjujemo pločice u 1D niz cijelih brojeva
private int[] tiles;
// Veličina pločice na sučelju
private int tileSize;
// Pozicija prazne pločice
private int blankPos;
// Margina za mrežu na okviru
private int margin;
// Veličina UI mreže
private int gridSize;
private boolean gameOver; // true ako je igra gotova, false inače
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// inicijalizacija pločica
nbTiles = size * size - 1; // -1 jer ne brojimo praznu pločicu
tiles = new int[size * size];
// izračunaj veličinu mreže i veličinu pločice
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) {
// koristi se za omogućavanje korisnicima interakciju na mreži klikom
// vrijeme je za implementaciju interakcije s korisnicima kako bi se pločice pomicale za rješavanje igre!
if (gameOver) {
newGame();
} else {
// dobivanje pozicije klika
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klik u mreži?
if (ex gridSize || ey gridSize)
return;
// dobivanje pozicije u mreži
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// dobivanje pozicije prazne ćelije
int c2 = blankPos % size;
int r2 = blankPos / size;
// pretvaramo u 1D koordinate
int clickPos = r1 * size + c1;
int dir = 0;
// tražimo smjer za višestruke pomake pločica odjednom
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) {
// pomičemo pločice u tom smjeru
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// provjeravamo je li igra riješena
gameOver = isSolved();
}
// ponovo crtamo panel
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // reset u početno stanje
shuffle(); // miješanje
} while(!isSolvable()); // ponavlja dok mreža ne bude rješiva
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;
}
}
// Samo polovina permutacija puzzla je rješiva.
// Kad god je pločica prethodila pločici s višom vrijednosti, to se računa
// kao obrnuto. U našem slučaju, s praznom pločicom u rješenju,
// broj obrnutih mora biti paran da bi se problem mogao riješiti.
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) // ako prazna pločica nije u rješenju => nije rješena
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("Igra od 15");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// centriraj na ekranu
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Napokon, igramo!
Vrijeme je da pokrenemo igru i vidimo kako funkcionira. Polje bi trebalo izgledati ovako:

Pokušavamo riješiti zagonetku. Ako je sve prošlo uspješno, dobijamo ovo:

I to je to. Jeste li očekivali više? 🙂
Skillbox preporučuje:
- Praktični tečaj .
- Prijateljski online tečaj .
- Dvoipogodišnji praktični tečaj .
Izvor: habr.com
