
„Piętnastka” to doskonały przykład prostej gry logicznej, popularnej na całym świecie. Aby rozwiązać łamigłówkę, należy ułożyć kwadraty z cyframi w kolejności, od najmniejszego do największego. To nie jest proste, ale ciekawe.
W dzisiejszym tutorialu pokazujemy, jak stworzyć „Piętnastkę” w Java 8 z Eclipse. Do stworzenia UI użyjemy Swing API.
Przypominamy: dla wszystkich czytelników „Habra” — zniżka 10 000 rubli przy zapisie na dowolny kurs Skillbox z kodem promocyjnym „Habra”.
Skillbox poleca: Edukacyjny kurs online .
Projektowanie gry
Na tym etapie należy określić właściwości:
- Size — rozmiar planszy;
- nbTiles — liczba kafelków na planszy. nbTiles = size*size — 1;
- Tiles — kafelek, który jest jednowymiarową tablicą liczb całkowitych. Każdy z kafelków otrzyma unikalną wartość w zakresie [0, nbTiles]. Zero oznacza pusty kwadrat;
- blankPos — pozycja pustego kwadratu.
Logika gry
Musimy określić metodę reset, używaną do inicjalizacji nowej pozycji gry. Ustawiamy tak wartość dla każdego elementu tablicy kafelków. A następnie umieszczamy blankPos na ostatniej pozycji tablicy.
Potrzebna jest również metoda shuffle do tasowania tablicy kafelków. Nie włączamy pustego kafelka w proces tasowania, aby pozostawić go w tej samej pozycji.
Ponieważ tylko połowa możliwych początkowych pozycji łamigłówki ma rozwiązanie, musimy sprawdzić uzyskany wynik tasowania, aby upewnić się, że bieżący układ jest w ogóle rozwiązywalny. W tym celu definiujemy metodę isSolvable.
Jeśli konkretnemu kafelkowi poprzedza kafelek o wyższej wartości, jest to nazywane inwersją. Gdy pusty kafelek znajduje się na swoim miejscu, liczba inwersji musi być parzysta, aby łamigłówka była rozwiązywalna. Zatem zliczamy liczbę inwersji i zwracamy true, jeśli liczba jest parzysta.
Następnie ważne jest określenie metody isSolved, aby sprawdzić, czy nasz układ Game Of Fifteen jest rozwiązany. Najpierw sprawdzamy, gdzie znajduje się pusty kafelek. Jeśli jest w początkowej pozycji, to bieżący układ to nowy, wcześniejszy niezrealizowany. Następnie przeglądamy kafelki w odwrotnej kolejności, a jeśli wartość kafelka różni się od odpowiedniego indeksu +1, zwracamy false. W przeciwnym razie na końcu metody czas zwrócić true, ponieważ łamigłówka jest już rozwiązana.
Kolejną metodą, którą należy zdefiniować, jest newGame. Służy ona do tworzenia nowego egzemplarza gry. W tym celu resetujemy planszę gry, następnie tasujemy ją i kontynuujemy, aż pozycja gry stanie się rozwiązywalna.
Oto przykład kodu z kluczową logiką piętnastek:
private void newGame() {
do {
reset(); // reset w stanie początkowym
shuffle(); // tasuj
} while(!isSolvable()); // powtarzaj, aż plansza będzie rozwiązywalna
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;
}
}
// Tylko połowa permutacji układanki jest rozwiązywalna
// Kiedy płytka jest poprzedzona płytką o wyższej wartości, liczy się jako inwersja.
// W naszym przypadku, z pustą płytką w rozwiązanej pozycji,
// liczba inwersji musi być parzysta, aby układanka była rozwiązywalna
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) // jeśli pusta płytka nie jest w rozwiązanej pozycji ==> nie rozwiązano
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Na koniec, należy zaimplementować ruch piętnastek w tablicy. Ten kod będzie wywoływany później przez wywołanie zwrotne (callback), aby reagować na ruch kursora. Nasza gra będzie obsługiwać wiele ruchów płytek jednocześnie. Tak więc, po przekształceniu klikniętej pozycji na ekranie w piętnastkę, uzyskujemy pozycję pustej piętnastki i szukamy kierunku ruchu, aby wspierać wiele przemieszczeń jednocześnie.
Oto przykład kodu:
// 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;Tworzymy UI w API Swing
Czas zająć się interfejsem. Najpierw bierzemy klasę Jpanel. Następnie rysujemy piętnastki na polu – do obliczenia wymiarów każdej wykorzystamy dane zadane w parametrze konstruktora gry:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margines jest również parametrem zadanym w konstruktorze gry.
Teraz należy zdefiniować metodę drawGrid, aby narysować siatkę i piętnastki na ekranie. Analizujemy tablicę piętnastek i przekształcamy współrzędne na współrzędne interfejsu użytkownika. Następnie narysujemy każdą piętnastkę z odpowiednim numerem na środku:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// przekształcamy 1D współrzędne na 2D, uwzględniając rozmiar tablicy 2D
int r = i / size;
int c = i % size;
// przekształcamy w współrzędne na UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// sprawdzamy specjalny przypadek pustego kafelka
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// dla innych kafelków
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 koniec nadpisujemy metodę paintComponent, będącą pochodną klasy JPane. Następnie wykorzystujemy metodę drawGrid, a potem metodę drawStartMessage, aby wyświetlić komunikat sugerujący kliknięcie w celu rozpoczęcia gry:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Kliknij, aby rozpocząć nową grę";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// centrowanie tekstu s dla danego kafelka (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);
}Reagujemy na działania użytkownika w interfejsie użytkownika
Aby gra mogła toczyć się w odpowiednim tempie, należy obsługiwać działania użytkownika w interfejsie. W tym celu dodajemy implementację MouseListener do Jpanel oraz kod do przesuwania kafelków, już wcześniej pokazany:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// używane do pozwolenia użytkownikom na interakcję na planszy przez kliknięcie
// czas zaimplementować interakcję z użytkownikami, aby przesuwać kafelki w celu rozwiązania gry!
if (gameOver) {
newGame();
} else {
// uzyskanie pozycji kliknięcia
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kliknięcie na planszy?
if (ex gridSize || ey gridSize)
return;
// uzyskanie pozycji na planszy
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// uzyskanie pozycji pustej komórki
int c2 = blankPos % size;
int r2 = blankPos / size;
// przekształcamy w współrzędne 1D
int clickPos = r1 * size + c1;
int dir = 0;
// szukamy kierunku dla wielokrotnego ruchu kafelków
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) {
// przesuwamy kafelki w kierunku
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// sprawdzamy, czy gra jest rozwiązana
gameOver = isSolved();
}
// malujemy panel ponownie
repaint();
}
});Kod umieszczamy w konstruktorze klasy GameOfFifteen. Na samym końcu wywołujemy metodę newGame, aby rozpocząć nową grę.
Pełny kod gry
Ostatni krok, zanim zobaczysz grę w akcji, polega na złożeniu wszystkich elementów kodu razem. Oto co otrzymasz:
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;
// Tworzymy grę w Puzzle 15 w Javie 8 i Swingu
// Jeśli masz jakieś pytania, przeczytaj komentarze ;)
public class GraW15 extends JPanel { // nasza plansza zostanie narysowana w dedykowanej Panel
// Rozmiar naszej instancji GraW15
private int rozmiar;
// Liczba kafli
private int liczbaKafli;
// Wymiary UI planszy
private int wymiar;
// Kolor przedni
private static final Color KOLOR_PRZEDNI = new Color(239, 83, 80); // używamy dowolnego koloru
// Obiekt losowy do tasowania kafli
private static final Random LOSOWY = new Random();
// Przechowywanie kafli w jednowymiarowej tablicy całkowitych
private int[] kafle;
// Rozmiar kafla w UI
private int rozmiarKafla;
// Pozycja pustego kafla
private int pustaPozycja;
// Margines dla planszy na ramce
private int margines;
// Rozmiar UI planszy
private int rozmiarPlanszy;
private boolean koniecGry; // true, jeśli gra skończona, false w przeciwnym razie
public GraW15(int rozmiar, int wym, int marg) {
this.rozmiar = rozmiar;
wymiar = wym;
margines = marg;
// inicjalizacja kafli
liczbaKafli = rozmiar * rozmiar - 1; // -1 ponieważ nie liczymy pustego kafla
kafle = new int[rozmiar * rozmiar];
// obliczanie rozmiaru planszy i rozmiaru kafla
rozmiarPlanszy = (wym - 2 * margines);
rozmiarKafla = rozmiarPlanszy / rozmiar;
setPreferredSize(new Dimension(wymiar, wymiar + margines));
setBackground(Color.WHITE);
setForeground(KOLOR_PRZEDNI);
setFont(new Font("SansSerif", Font.BOLD, 60));
koniecGry = true;
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// używane do interakcji użytkowników z planszą klikając
// czas zaimplementować interakcje z użytkownikami, aby przesuwać kafle w celu rozwiązania gry!
if (koniecGry) {
nowaGra();
} else {
// uzyskanie pozycji kliknięcia
int ex = e.getX() - margines;
int ey = e.getY() - margines;
// kliknięcie na planszy?
if (ex rozmiarPlanszy || ey rozmiarPlanszy)
return;
// uzyskanie pozycji w planszy
int c1 = ex / rozmiarKafla;
int r1 = ey / rozmiarKafla;
// uzyskanie pozycji pustej komórki
int c2 = pustaPozycja % rozmiar;
int r2 = pustaPozycja / rozmiar;
// konwertujemy na 1D
int pozycjaKlikniecia = r1 * rozmiar + c1;
int kierunek = 0;
// szukamy kierunku dla wielokrotnego ruchu kafli jednocześnie
if (c1 == c2 && Math.abs(r1 - r2) > 0)
kierunek = (r1 - r2) > 0 ? rozmiar : -rozmiar;
else if (r1 == r2 && Math.abs(c1 - c2) > 0)
kierunek = (c1 - c2) > 0 ? 1 : -1;
if (kierunek != 0) {
// przesuwamy kafle w kierunku
do {
int nowaPustaPozycja = pustaPozycja + kierunek;
kafle[pustaPozycja] = kafle[nowaPustaPozycja];
pustaPozycja = nowaPustaPozycja;
} while(pustaPozycja != pozycjaKlikniecia);
kafle[pustaPozycja] = 0;
}
// sprawdzamy, czy gra jest rozwiązana
koniecGry = jestRozwiazana();
}
// odświeżamy panel
repaint();
}
});
nowaGra();
}
private void nowaGra() {
do {
reset(); // reset do stanu początkowego
tasowanie(); // tasowanie
} while(!jestRozwiazana()); // powtarzamy, aż plansza będzie rozwiązana
koniecGry = false;
}
private void reset() {
for (int i = 0; i 1) {
int r = LOSOWY.nextInt(n--);
int tmp = kafle[r];
kafle[r] = kafle[n];
kafle[n] = tmp;
}
}
// Tylko połowa permutacji układanki jest rozwiązywalna.
// Za każdym razem, gdy kafel jest poprzedzony kaflem o wyższej wartości, liczy się
// jako inwersja. W naszym przypadku, gdy pusty kafel jest w rozwiązanej pozycji,
// liczba inwersji musi być parzysta, aby układanka była rozwiązywalna.
private boolean jestRozwiazana() {
int liczbaInwersji = 0;
for (int i = 0; i < liczbaKafli; i++) {
for (int j = 0; j kafle[i])
liczbaInwersji++;
}
}
return liczbaInwersji % 2 == 0;
}
private boolean jestRozwiazana() {
if (kafle[kafle.length - 1] != 0) // jeśli pusty kafel nie jest w rozwiązanej pozycji => nie rozwiązana
return false;
for (int i = liczbaKafli - 1; i >= 0; i--) {
if (kafle[i] != i + 1)
return false;
}
return true;
}
private void rysujPlansze(Graphics2D g) {
for (int i = 0; i {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("Gra w 15");
frame.setResizable(false);
frame.add(new GraW15(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// centrowanie na ekranie
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Wreszcie gramy!
Czas uruchomić grę i sprawdzić, jak działa. Plansza powinna wyglądać następująco:

Próbujemy rozwiązać zagadkę. Jeśli wszystko poszło pomyślnie, otrzymujemy to:

I to wszystko. Spodziewałeś się więcej? 🙂
Skillbox poleca:
- Praktyczny kurs .
- Kurs online .
- Dwuletni praktyczny kurs .
Źródło: habr.com
