
„Patnáct“, nebo „Patnáctka“ — skvělý příklad jednoduché logické hry, která je populární po celém světě. Abyste vyřešili tuto hádanku, musíte uspořádat čtverce s čísly v pořadí od nejmenšího po největší. Není to snadné, ale je to zajímavé.
V dnešním tutoriálu ukážeme, jak vyvinout „Patnáctku“ v Javě 8 s Eclipse. Pro vývoj UI použijeme Swing API.
Připomínáme: pro všechny čtenáře "Habra" – sleva 10 000 korun při registraci na jakýkoli kurz Skillbox s promokódem "Habr".
Skillbox doporučuje: Vzdělávací online kurz .
Navrhování hry
V této fázi je třeba určit vlastnosti:
- Size — velikost herního pole;
- nbTiles — počet patnáctek na poli. nbTiles = size*size — 1;
- Tiles — patnáctka, která představuje jednorozměrné pole celých čísel. Každá patnáctka obdrží unikátní hodnotu v rozsahu [0, nbTiles]. Nula označuje prázdný čtverec;
- blankPos — pozice prázdného čtverce.
Logika hry
Je třeba definovat metodu reset, která se používá k inicializaci nové herní pozice. Tím nastavíme hodnotu pro každý prvek pole patnáctek. Potom umístíme blankPos na poslední pozici pole.
Také potřebujeme metodu shuffle pro zamíchání pole patnáctek. Nezapojujeme prázdnou patnáctku do procesu míchání, abychom ji ponechali ve stejné pozici.
Vzhledem k tomu, že pouze polovina možných výchozích pozic hádanky má řešení, je třeba zkontrolovat výsledný stav zamíchání, aby se ujistilo, že aktuální uspořádání je vůbec řešitelné. K tomu definujeme metodu isSolvable.
Pokud konkrétní patnáctku předchází patnáctka s vyšší hodnotou, považuje se to za inverzi. Když je prázdná patnáctka na svém místě, musí být počet inverzí sudý, aby byla hádanka řešitelná. Takže počítáme počet inverzí a vracíme true, pokud je počet sudý.
Pak je důležité definovat metodu isSolved, abychom zkontrolovali, zda je naše uspořádání Game Of Fifteen vyřešeno. Nejprve se podíváme, kde se nachází prázdná patnáctka. Pokud je na výchozí pozici, je aktuální uspořádání nové, dosud nevyřešené. Poté procházíme dlaždice v opačném pořadí a pokud se hodnota patnáctky liší od odpovídajícího indexu +1, vrátíme false. Jinak na konci metody vrátíme true, protože hádanka je už vyřešena.
Další metodou, kterou je třeba určit, je newGame. Ta je potřebná pro vytvoření nové instance hry. K tomu resetujeme hrací pole, poté ho zamícháme a pokračujeme, dokud není hrací pozice řešitelná.
Zde je příklad kódu s klíčovou logikou patnáctek:
private void newGame() {
do {
reset(); // resetovat do výchozího stavu
shuffle(); // zamíchat
} while (!isSolvable()); // pokračovat, dokud nebude mřížka řešitelná
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;
}
}
// Pouze polovina permutací puzzle je řešitelná/
// Kdykoli je dlaždice předcházená dlaždicí s vyšší hodnotou, počítá se
// jako inverze. V našem případě, s prázdnou dlaždicí v řešené pozici,
// počet inverzí musí být sudý, aby bylo puzzle řešitelné
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) // pokud prázdná dlaždice není v řešené pozici => není vyřešeno
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Nakonec je třeba naprogramovat pohyb patnáctek v poli. Tento kód bude později vyvolán pomocí zpětného volání (callback), aby reagoval na pohyb kurzoru. Naše hra bude podporovat více pohybů dlaždic zároveň. Jakmile převedeme stisknutou pozici na obrazovce na patnáctku, získáme pozici prázdné dlaždice a hledáme směr pohybu, abychom podpořili její více pohybů současně.
Zde je pří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íjíme UI na Swing API
Je čas zabývat se uživatelským rozhraním. Nejprve vezmeme třídu JPanel. Poté na poli vykreslíme patnáctky — pro výpočet rozměrů každé použijeme údaje zadané v parametru konstruktora hry:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin je také parametrem zadaným v konstruktoru hry.
Nyní je třeba definovat metodu drawGrid pro vykreslení sítě a patnáctek na obrazovce. Analyzujeme pole patnáctek a převádíme souřadnice na souřadnice uživatelského rozhraní. Poté vykreslíme každou patnáctku s odpovídajícím číslem uprostřed:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// převádíme 1D souřadnice na 2D souřadnice podle velikosti 2D pole
int r = i / size;
int c = i % size;
// převádíme souřadnice na UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// kontrolujeme speciální případ pro prázdný dlaždici
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// pro 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);
}
}Nakonec přepíšeme metodu paintComponent, která je odvozená od třídy JPane. Poté použijeme metodu drawGrid a po ní metodu drawStartMessage pro zobrazení zprávy, která vyzývá k kliknutí pro spuštění hry:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Klikněte pro zahájení nové hry";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// středíme text s pro danou dlaždici (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 akce uživatele v UI
Aby hra probíhala bez problémů, je třeba zpracovávat akce uživatele v UI. K tomu přidáme implementaci MouseListener na Jpanel a kód pro pohyb dlaždic, jak bylo již uvedeno výše:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// používané k tomu, aby uživatelé mohli interagovat s mřížkou kliknutím
// je čas implementovat interakci s uživateli pro pohyb dlaždic k vyřešení hry!
if (gameOver) {
newGame();
} else {
// zjistíme pozici kliknutí
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kliknutí v mřížce?
if (ex gridSize || ey gridSize)
return;
// zjistíme pozici v mřížce
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// zjistíme pozici prázdné buňky
int c2 = blankPos % size;
int r2 = blankPos / size;
// převádíme na 1D souřadnici
int clickPos = r1 * size + c1;
int dir = 0;
// hledáme směr pro vícenásobné pohyby dlaždic
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) {
// posuneme dlaždice ve směru
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// kontrolujeme, jestli je hra vyřešena
gameOver = isSolved();
}
// překreslíme panel
repaint();
}
});Kód umisťujeme do konstruktoru třídy GameOfFifteen. Na konci voláme metodu newGame pro zahájení nové hry.
Úplný kód hry
Poslední krok, než uvidíte hru v akci, je spojit všechny části kódu dohromady. Toto je výsledek:
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;
// Vytvoříme Hru Dvaceti Pěti Puzzle s Javam 8 a Swingem
// Pokud máte nějaké dotazy, neváhejte si přečíst komentáře ;)
public class GameOfFifteen extends JPanel { // naši mřížku nakreslíme na dedikované Pane
// Velikost naší instance Hry Dvaceti Pěti
private int size;
// Počet dlaždic
private int nbTiles;
// Rozměr UI mřížky
private int dimension;
// Barva popředí
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // používáme libovolnou barvu
// Náhodný objekt pro zamíchání dlaždic
private static final Random RANDOM = new Random();
// Ukládání dlaždic v jednorozměrném poli celých čísel
private int[] tiles;
// Velikost dlaždice na UI
private int tileSize;
// Pozice prázdné dlaždice
private int blankPos;
// Okraj pro mřížku na rámečku
private int margin;
// Velikost UI mřížky
private int gridSize;
private boolean gameOver; // true pokud je hra u konce, false jinak
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// inicializace dlaždic
nbTiles = size * size - 1; // -1 protože nepočítáme prázdnou dlaždici
tiles = new int[size * size];
// výpočet velikosti mřížky a dlaždic
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žívá se k tomu, aby uživatelé mohli interagovat s mřížkou kliknutím
// je čas implementovat interakci s uživateli pro posunutí dlaždic a vyřešení hry !
if (gameOver) {
newGame();
} else {
// zjistit pozici kliknutí
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kliknutí v mřížce ?
if (ex gridSize || ey gridSize)
return;
// zjistit pozici v mřížce
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// zjistit pozici prázdné buňky
int c2 = blankPos % size;
int r2 = blankPos / size;
// převedeme na 1D souřadnici
int clickPos = r1 * size + c1;
int dir = 0;
// hledáme směr pro více pohybů dlaždic najednou
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) {
// posuneme dlaždice ve směru
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// zkontrolujeme, zda je hra vyřešena
gameOver = isSolved();
}
// repaintujeme panel
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // reset do počátečního stavu
shuffle(); // zamíchání
} while(!isSolvable()); // děláme to dokud není mřížka řešitelná
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;
}
}
// Pouze polovina permutací puzzle je řešitelná.
// Kdykoli je dlaždice předcházená dlaždicí s vyšší hodnotou, počítá se to
// jako inverze. V našem případě, kdy je prázdná dlaždice v vyřešené pozici,
// musí být počet inverzí sudý, aby bylo puzzle řešitelné
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) // pokud prázdná dlaždice není na vyřešené pozici ==> není vyřešeno
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 Dvaceti Pěti");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// centrovat na obrazovku
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Konečně hrajeme!
Je nejvyšší čas spustit hru a vyzkoušet ji v akci. Pole by mělo vypadat takto:

Snažíme se vyřešit hádanku. Pokud to vše proběhlo úspěšně, dostaneme následující:

A to je vše. Očekávali jste víc? 🙂
Skillbox doporučuje:
- Praktický kurz .
- Aplikovaný online kurz .
- Dvouroční praktický kurz .
Zdroj: habr.com
