
„Cincisprezece”, sau „Cincime” — este un exemplu excelent de joc logic simplu, popular în întreaga lume. Pentru a rezolva puzzle-ul, este necesar să aranjezi căsuțele cu numere în ordinea crescătoare. Nu este simplu, dar este interesant.
În tutorialul de astăzi, vom arăta cum să dezvoltăm „Cincisprezece” în Java 8 cu Eclipse. Pentru dezvoltarea UI, vom folosi Swing API.
Vă reamintim: pentru toți cititorii „Habr” — reducere de 10.000 de ruble la înscrierea la orice curs Skillbox cu codul de promovare „Habr”.
Skillbox recomandă: Curs online educațional .
Proiectarea jocului
La acest pas trebuie să definim proprietățile:
- Size — dimensiunea câmpului de joc;
- nbTiles — numărul de căsuțe în câmp. nbTiles = size*size — 1;
- Tiles — căsuța, care reprezintă un array unidimensional de numere întregi. Fiecare căsuță va primi o valoare unică în intervalul [0, nbTiles]. Zero indică căsuța goală;
- blankPos — poziția căsuței goale.
Logica jocului
Trebuie să definim metoda reset, folosită pentru a inițializa o nouă poziție de joc. Astfel, stabilim valoarea pentru fiecare element al array-ului de căsuțe. Apoi, plasăm blankPos în ultima poziție a array-ului.
De asemenea, este necesară o metodă shuffle pentru a amesteca array-ul de căsuțe. Nu includem căsuța goală în procesul de amestecare, pentru a o menține în aceeași poziție.
Deoarece doar jumătate din posibilele poziții de start ale puzzle-ului au soluție, trebuie să verificăm rezultatul obținut prin amestecare pentru a ne asigura că aranjamentul curent este, în general, rezolvabil. Pentru a face asta, definim metoda isSolvable.
Dacă o căsuță specifică este precedată de o căsuță cu o valoare mai mare, aceasta este considerată o inversare. Când căsuța goală este la locul ei, numărul inversărilor trebuie să fie par pentru ca puzzle-ul să fie rezolvabil. Așadar, numărăm inversările și returnăm true dacă numărul este par.
Apoi, este important să definim metoda isSolved, pentru a verifica dacă aranjamentul nostru Game Of Fifteen este rezolvat. Mai întâi, ne uităm unde se află căsuța goală. Dacă este în poziția inițială, atunci aranjamentul curent este nou, nerezolvat anterior. Apoi, verificăm căsuțele în ordine inversă, iar dacă valoarea căsuței diferă de indicele corespunzător +1, returnăm false. În caz contrar, la finalul metodei putem returna true, deoarece puzzle-ul este deja rezolvat.
O altă metodă care trebuie definită este newGame. Aceasta este folosită pentru a crea o nouă instanță a jocului. Pentru aceasta, resetăm tabla de joc, apoi o amestecăm și continuăm până când poziția de joc devine solvabilă.
Iată un exemplu de cod cu logica cheie a jocului 15:
private void newGame() {
do {
reset(); // resetare în starea inițială
shuffle(); // amestecare
} while(!isSolvable()); // continuăm până când tabla devine solvabilă
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;
}
}
// Numai jumătate din permutările puzzle-ului sunt solvabile/
// Ori de câte ori o placă este precedată de o placă cu o valoare mai mare, aceasta contează
// ca și o inversare. În cazul nostru, cu placa goală în poziția rezolvată,
// numărul inversărilor trebuie să fie par pentru ca puzzle-ul să fie solvabil
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) // dacă placa goală nu este în poziția rezolvată => nu este rezolvat
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}În cele din urmă, trebuie să programăm mișcarea plăcilor în tablou. Acest cod va fi apelat mai târziu printr-un callback pentru a reacționa la mișcarea cursorului. Jocul nostru va suporta mai multe mișcări ale plăcilor simultan. Astfel, după ce am convertit poziția apăsată pe ecran în placa corespunzătoare, obținem poziția plăcii goale și căutăm direcția de mișcare pentru a suporta mai multe mișcări simultan.
Iată un exemplu de cod:
// 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;Dezvoltăm UI folosind Swing API
Este momentul să ne ocupăm de interfață. Mai întâi, luăm clasa Jpanel. Apoi desenăm plăcile pe tablă—pentru a calcula dimensiunile fiecărei plăci, vom folosi datele specificate în parametrul constructorului jocului:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Marginea este de asemenea un parametru specificat în constructorul jocului.
Acum trebuie să definim metoda drawGrid pentru a desena grila și plăcile pe ecran. Analizăm tabloul plăcilor și convertim coordonatele în coordonatele interfeței utilizatorului. Apoi vom desena fiecare placă cu numărul corespunzător în centru:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// convertim coordonatele 1D în coordonate 2D, având în vedere dimensiunea matricei 2D
int r = i / size;
int c = i % size;
// convertim în coordonate pentru UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// verificăm cazul special pentru placa goală
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// pentru celelalte plăci
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);
}
}În cele din urmă, vom suprascrie metoda paintComponent, care este derivată din clasa JPane. Apoi vom folosi metoda drawGrid și, ulterior, metoda drawStartMessage pentru a afișa un mesaj care invită să facem clic pentru a începe jocul:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Clic pentru a începe un joc nou";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// centram șirul s pentru placa dată (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);
}Reacționăm la acțiunile utilizatorului în UI
Pentru ca jocul să decurgă bine, este necesar să procesăm acțiunile utilizatorului în UI. Pentru aceasta, adăugăm implementarea MouseListener pe Jpanel și codul pentru mutarea plăcilor, deja arătat mai sus:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// folosit pentru a permite utilizatorilor să interacționeze cu tabla prin clic
// este timpul să implementăm interacțiunea cu utilizatorii pentru a muta plăcile pentru a rezolva jocul!
if (gameOver) {
newGame();
} else {
// obțineți poziția clicului
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// clic în tablă?
if (ex gridSize || ey gridSize)
return;
// obțineți poziția în tablă
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// obțineți poziția celulei goale
int c2 = blankPos % size;
int r2 = blankPos / size;
// convertim în coordonata 1D
int clickPos = r1 * size + c1;
int dir = 0;
// căutăm direcția pentru mișcările multiple ale plăcilor
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) {
// mutăm plăcile în direcția specificată
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// verificăm dacă jocul este rezolvat
gameOver = isSolved();
}
// repaintăm panelul
repaint();
}
});Codul este plasat în constructorul clasei GameOfFifteen. La sfârșit, apelăm metoda newGame pentru a începe un joc nou.
Codul complet al jocului
Ultimul pas, înainte de a vedea jocul în acțiune, este să adunăm toate elementele codului împreună. Iată ce va ieș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;
// Vom crea un joc de puzzle 15 cu Java 8 și Swing
// Dacă aveți întrebări, nu ezitați să citiți comentariile ;)
public class GameOfFifteen extends JPanel { // grila noastră va fi desenată într-un panou dedicat
// Dimensiunea instanței noastre de joc 15
private int size;
// Numărul de plăci
private int nbTiles;
// Dimensiunea UI a grilei
private int dimension;
// Culoarea de prim-plan
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // folosim o culoare arbitrară
// Obiect aleator pentru amestecarea plăcilor
private static final Random RANDOM = new Random();
// Stocarea plăcilor într-un tablou 1D de întregi
private int[] tiles;
// Dimensiunea plăcii în UI
private int tileSize;
// Poziția plăcii goale
private int blankPos;
// Marginea pentru grilă pe cadru
private int margin;
// Dimensiunea UI a grilei
private int gridSize;
private boolean gameOver; // adevărat dacă jocul s-a Terminat, fals în caz contrar
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// inițializăm plăcile
nbTiles = size * size - 1; // -1 pentru că nu numărăm placa goală
tiles = new int[size * size];
// calculăm dimensiunea grilei și dimensiunea plăcii
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) {
// folosit pentru a permite utilizatorilor să interacționeze cu grila prin clic
// e timpul să implementăm interacțiunea cu utilizatorii pentru a muta plăcile și a rezolva jocul!
if (gameOver) {
newGame();
} else {
// obținem poziția clicului
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// clic în grilă?
if (ex gridSize || ey gridSize)
return;
// obținem poziția în grilă
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// obținem poziția celulei goale
int c2 = blankPos % size;
int r2 = blankPos / size;
// convertim în coordonatele 1D
int clickPos = r1 * size + c1;
int dir = 0;
// căutăm direcția pentru mai multe mutări de plăci simultan
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) {
// mutăm plăcile în direcție
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// verificăm dacă jocul este rezolvat
gameOver = isSolved();
}
// repaintăm panelul
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // resetăm în starea inițială
shuffle(); // amestecăm
} while(!isSolvable()); // continuăm până când grila devine soluționabilă
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;
}
}
// Doar jumătate din permutațiile puzzle-ului sunt soluționabile.
// Oricând o placă este precedată de o placă cu o valoare mai mare, este considerată
// o inversare. În cazul nostru, cu placa goală în poziția rezolvată,
// numărul inversărilor trebuie să fie par pentru ca puzzle-ul să fie soluționabil.
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) // dacă placa goală nu este în poziția rezolvată => nu este rezolvat
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("Game of Fifteen");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// centrat pe ecran
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}În sfârșit, să jucăm!
Este momentul perfect să lansăm jocul și să-i vedem acțiunea. Terenul ar trebui să arate astfel:

Încercăm să rezolvăm puzzle-ul. Dacă totul a mers bine, obținem următoarele rezultate:

Asta e tot. Te așteptai la mai mult? 🙂
Skillbox recomandă:
- Curs practic .
- Curs online aplicat .
- Curs practic de doi ani .
Sursa: habr.com
