
„Pätnásť“, alebo „Pätnástačka“ — je skvelý príklad jednoduchej logickej hry, populárnej po celom svete. Na vyriešenie hádanky je potrebné usporiadať štvrtky s číslami v poradí od najmenšieho po najväčšie. Nie je to jednoduché, ale je to zaujímavé.
V dnešnom tutoriáli ukážeme, ako vyvinúť „Pätnástačku“ v Jave 8 s Eclipse. Na vývoj UI použijeme Swing API.
Pripomíname: pre všetkých čitateľov „Habr“ — zľava 10 000 rubľov pri zapísaní na akýkoľvek kurz Skillbox s promo kódom „Habr“.
Skillbox odporúča: Vzdelávací online kurz .
Návrh hry
V tejto fáze je potrebné určiť vlastnosti:
- Size — veľkosť herného poľa;
- nbTiles — počet pätnástok na poli. nbTiles = size * size — 1;
- Tiles — pätnástka, ktorá je jednorozmerným poľom celých čísel. Každá z pätnástok dostane jedinečnú hodnotu v rozsahu [0, nbTiles]. Nula označuje prázdny štvorček;
- blankPos — pozícia prázdneho štvorčeka.
Logika hry
Je potrebné definovať metódu na resetovanie (reset), ktorá sa používa na inicializáciu novej hernej pozície. Takto nastavujeme hodnotu pre každý prvok poľa pätnástok. A potom umiestnime blankPos na poslednú pozíciu poľa.
Tiež potrebujeme metódu shuffle na premiešanie poľa pätnástok. Nezahŕňame prázdnu pätnástku do procesu premiešania, aby sme ju nechali na jej pôvodnom mieste.
Keďže len polovica možných štartovacích pozícií hádanky má riešenie, je potrebné overiť výsledok premiešania, aby sme sa uistili, že aktuálne rozloženie je vôbec riešiteľné. Na to definujeme metódu isSolvable.
Ak konkrétnej pätnástke predchádza pätnástka s vyššou hodnotou, považuje sa to za inverziu. Keď je prázdna pätnástka na svojom mieste, počet inverzií musí byť párny, aby bola hádanka riešiteľná. Takže počítame počet inverzií a vraciame true, ak je číslo párne.
Potom je dôležité definovať metódu isSolved, aby sme zistili, či je naše rozloženie Game Of Fifteen vyriešené. Najprv sa pozrieme, kde sa nachádza prázdna pätnástka. Ak je na počiatočnej pozícii, aktuálne rozloženie je nové, doteraz nevyriešené. Potom prechádzame cez dlaždice v opačnom poradí a ak hodnota pätnástky nezodpovedá príslušnému indexu + 1, vraciame false. V opačnom prípade, na konci metódy, môžeme vrátiť true, pretože hádanka je už vyriešená.
Ďalšou metódou, ktorú je potrebné definovať, je newGame. Tento metóda je potrebná na vytvorenie nového inštancie hry. Na to resetujeme hernú plochu, potom ju zamiešame a pokračujeme, kým herná pozícia nebude riešiteľná.
Tu je príklad kódu s kľúčovou logikou pre patnástky:
private void newGame() {
do {
reset(); // reset v počiatočnom stave
shuffle(); // zamiešať
} while(!isSolvable()); // pokračovať, kým nebude mriežka riešiteľná
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;
}
}
// Iba polovica permutácií puzzle je riešiteľná/
// Kedykoľvek je dlaždica predchádzaná dlaždicou s vyššou hodnotou, počíta sa
// ako inverzia. V našom prípade, keď je prázdna dlaždica na vyriešenej pozícii,
// počet inverzií musí byť párny, aby bolo puzzle riešiteľné
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) // ak prázdna dlaždica nie je na vyriešenej pozícii == > nie je vyriešené
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Nakoniec je potrebné naprogramovať pohyb dlaždíc v poli. Tento kód bude vyvolaný neskôr pomocou zpětného volania (callback), aby reagoval na pohyb kurzora. Naša hra bude podporovať viacero pohybov dlaždíc súčasne. Po tom, ako prekonvertujeme stlačenú pozíciu na obrazovke na dlaždicu, získame pozíciu prázdnej dlaždice a vyhľadáme smer pohybu na podporu viacerých jej pohybov súčasne.
Tu je príklad kódu:
// 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;Vyvíjame UI na Swing API
Je čas zamerať sa na rozhranie. Najprv vezmime triedu JPanel. Potom na plochu nakreslíme dlaždice – na výpočet rozmerov každej využijeme údaje zadané v konštruktore hry:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin je tiež parameter zadaný v konštruktore hry.
Teraz je potrebné definovať metódu drawGrid na vykreslenie mriežky a dlaždíc na obrazovke. Analyzujeme pole dlaždíc a konvertujeme súradnice na súradnice používateľského rozhrania. Potom vykreslíme každú dlaždicu s príslušným číslom v strede:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// prevádzame 1D súradnice na 2D súradnice vzhľadom na veľkosť 2D poľa
int r = i / size;
int c = i % size;
// prevádzame súradnice na UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// kontrolujeme špeciálny prípad prázdneho dlaždice
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// pre ostatné dlaždice
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);
}
}Nakoniec predefinujeme metódu paintComponent, ktorá je odvodená z triedy JPane. Potom použijeme metódu drawGrid a následne metódu drawStartMessage na zobrazenie správy, ktorá ponúka kliknúť na spustenie hry:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Kliknite na začiatok novej hry";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// centrovať reťazec s pre danú dlaždicu (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);
}Reagujeme na akcie používateľa v UI.
Pre to, aby hra plynula svojím vlastným spôsobom, je potrebné spracovávať akcie používateľa v UI. Na to pridáme implementáciu MouseListener na Jpanel a kód na posun dlaždíc, ktorý bol už vyššie znázornený:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// použitý na umožnenie interakcie používateľov s mriežkou kliknutím
// je čas implementovať interakciu používateľov na presun dlaždíc na vyriešenie hry!
if (gameOver) {
newGame();
} else {
// získanie pozície kliknutia
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kliknutím v mriežke?
if (ex gridSize || ey gridSize)
return;
// získanie pozície v mriežke
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// získanie pozície prázdnej bunky
int c2 = blankPos % size;
int r2 = blankPos / size;
// prevádzame do 1D súradnice
int clickPos = r1 * size + c1;
int dir = 0;
// hľadáme smer pre viacnásobný presun dlaždíc naraz
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) {
// presunieme dlaždice v smere
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// kontrolujeme, či je hra vyriešená
gameOver = isSolved();
}
// prekreslíme panel
repaint();
}
});Kód umiestnite do konštruktora triedy GameOfFifteen. Na konci zavoláme metódu newGame na spustenie novej hry.
Úplný kód hry
Posledný krok predtým, ako uvidíte hru v akcii, je potrebné poskladať všetky prvky kódu dohromady. Tu je, čo dostanete:
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;
// Vytvoríme Hru s 15 Puzzle v Jave 8 a Swing
// Ak máte nejaké otázky, neváhajte si prečítať komentáre ;)
public class GameOfFifteen extends JPanel { // náš mriežka bude vykreslená v prednej paneli
// Veľkosť našej inštancie hry s pätnástimi
private int size;
// Počet dlaždíc
private int nbTiles;
// Rozmer UI mriežky
private int dimension;
// Predná farba
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // používame ľubovoľnú farbu
// Náhodný objekt na premiešanie dlaždíc
private static final Random RANDOM = new Random();
// Ukladanie dlaždíc v 1D poli celých čísel
private int[] tiles;
// Veľkosť dlaždice na UI
private int tileSize;
// Poloha prázdnej dlaždice
private int blankPos;
// Okraj pre mriežku na ráme
private int margin;
// Veľkosť UI mriežky
private int gridSize;
private boolean gameOver; // true ak je hra dokončená, false inak
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// inicializácia dlaždíc
nbTiles = size * size - 1; // -1, pretože nepočítame prázdnu dlaždicu
tiles = new int[size * size];
// výpočet veľkosti mriežky a veľkosti dlaždice
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) {
// použité na interakciu užívateľov s mriežkou kliknutím
// je čas implementovať interakciu s užívateľmi na presunutie dlaždíc na vyriešenie hry!
if (gameOver) {
newGame();
} else {
// získať pozíciu kliknutia
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kliknutie v mriežke?
if (ex gridSize || ey gridSize)
return;
// získať pozíciu v mriežke
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// získať pozíciu prázdnej bunky
int c2 = blankPos % size;
int r2 = blankPos / size;
// konvertujeme na 1D súradnice
int clickPos = r1 * size + c1;
int dir = 0;
// hľadáme smer pre viacnásobné pohyby dlaždíc súčasne
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) {
// posúvame dlaždice v smere
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// kontrolujeme, či je hra vyriešená
gameOver = isSolved();
}
// znovu vykreslíme panel
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // reset na počiatočný stav
shuffle(); // premiešať
} while(!isSolvable()); // iba ak bude mriežka riešiteľná
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;
}
}
// Iba polovica permutácií puzzle je riešiteľná.
// Kedykoľvek je dlaždica predchádzaná dlaždicou s vyššou hodnotou, počíta sa
// ako inverzia. V našom prípade, s prázdnou dlaždicou na vyriešenom mieste,
// počet inverzií musí byť párny na to, aby bolo puzzle riešiteľné
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) // ak nie je prázdna dlaždica na vyriešenom mieste ==> nie je vyriešená
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("Hra s pätnástimi");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// centrum na obrazovke
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Konečne, hra je tu!
Teraz je ten správny čas spustiť hru a otestovať ju v akcii. Pole by malo vyzerať nasledovne:

Snažíme sa vyriešiť rébus. Ak všetko prebehlo úspešne, dostávame toto:

A to je všetko. Očakávali ste viac? 🙂
Skillbox odporúča:
- Praktický kurz .
- Aplikovaný online kurz .
- Dvojročný praktický kurz .
Zdroj: habr.com
