
«Quindici», o «Quindicina» — un ottimo esempio di un semplice gioco logico, popolare in tutto il mondo. Per risolvere il rompicapo, è necessario disporre i quadrati con i numeri in ordine, dal più piccolo al più grande. Non è facile, ma è interessante.
Nel tutorial di oggi mostriamo come sviluppare «Quindicina» su Java 8 con Eclipse. Per lo sviluppo dell'interfaccia utente utilizzeremo Swing API.
Ricordiamo: per tutti i lettori di «Habr» — sconto di 10.000 rubli per l'iscrizione a qualsiasi corso Skillbox con il codice promozionale «Habr».
Skillbox consiglia: Corso online educativo .
Progettazione del gioco
In questa fase è necessario definire le proprietà:
- Size — dimensione del campo di gioco;
- nbTiles — numero di tasselli nel campo. nbTiles = size*size — 1;
- Tiles — quindicina, che rappresenta un array unidimensionale di numeri interi. Ognuna delle quindicine avrà un valore unico nell'intervallo [0, nbTiles]. Lo zero indica il quadrato vuoto;
- blankPos — posizione del quadrato vuoto.
Logica del gioco
È necessario definire il metodo di ripristino (reset), utilizzato per inizializzare una nuova posizione di gioco. In questo modo impostiamo un valore per ciascun elemento dell'array delle quindicine. Poi posizioniamo blankPos nell'ultima posizione dell'array.
Servirà anche un metodo di mescolamento per mescolare l'array delle quindicine. Non includiamo la quindicina vuota nel processo di mescolamento per mantenerla nella sua posizione originale.
Poiché solo metà delle possibili posizioni iniziali del rompicapo ha una soluzione, è necessario verificare il risultato ottenuto dalla mescolanza per assicurarsi che la disposizione attuale sia effettivamente risolvibile. A tal fine, definiamo il metodo isSolvable.
Se una certa quindicina è preceduta da una quindicina con un valore più alto, questo è considerato un'inversione. Quando la quindicina vuota si trova al suo posto, il numero di inversioni deve essere pari affinché il rompicapo sia risolvibile. Quindi, contiamo il numero di inversioni e restituiamo true se il numero è pari.
È quindi importante definire il metodo isSolved per verificare se la nostra disposizione del Gioco dei Quindici è risolta. Iniziamo osservando dove si trova la quindicina vuota. Se nella posizione iniziale, allora la disposizione attuale è nuova, non risolta in precedenza. Poi scorriamo le tessere in ordine inverso e, se il valore della quindicina è diverso dall'indice corrispondente +1, restituiamo false. Altrimenti, alla fine del metodo, è giunto il momento di restituire true, perché il rompicapo è già risolto.
Un altro metodo da definire è newGame. Questo è necessario per creare una nuova istanza del gioco. Per fare ciò, ripristiniamo il campo di gioco, lo mescoliamo e continuiamo finché la posizione di gioco non sarà risolvibile.
Ecco un esempio di codice con la logica chiave dei quindici:
private void newGame() {
do {
reset(); // ripristina allo stato iniziale
shuffle(); // mescola
} while(!isSolvable()); // continua finché la griglia non è risolvibile
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;
}
}
// Solo metà delle permutazioni del puzzle sono risolvibili/
// Ogni volta che una tessera è preceduta da una tessera con un valore più alto, conta
// come un'inversione. Nel nostro caso, con la tessera vuota nella posizione risolta,
// il numero di inversioni deve essere pari affinché il puzzle sia risolvibile
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) // se la tessera vuota non è nella posizione risolta => non risolto
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Infine, dobbiamo programmare il movimento delle tessere nel array. Questo codice verrà chiamato in seguito tramite callback, per rispondere al movimento del cursore. Il nostro gioco supporterà più movimenti delle tessere simultaneamente. Quindi, dopo aver convertito la posizione toccata sullo schermo nella tessera, otteniamo la posizione della tessera vuota e cerchiamo la direzione del movimento per supportare più spostamenti contemporaneamente.
Ecco un esempio di codice:
// 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;Sviluppiamo l'interfaccia utente con l'API Swing
È ora di lavorare sull'interfaccia. Per prima cosa prendiamo la classe JPanel. Poi disegniamo le tessere sul campo — per calcolare le dimensioni di ciascuna utilizzeremo i dati forniti nel parametro del costruttore del gioco:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Il margine è anche un parametro fornito nel costruttore del gioco.
Ora dobbiamo definire il metodo drawGrid per disegnare la griglia e le tessere sullo schermo. Analizziamo l'array delle tessere e convertiamo le coordinate in coordinate dell'interfaccia utente. Poi disegniamo ogni tessera con il numero corrispondente al centro:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// convertiamo le coordinate 1D in coordinate 2D date le dimensioni dell'array 2D
int r = i / size;
int c = i % size;
// convertiamo in coordinate sull'interfaccia utente
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// controlla il caso speciale per la tessera vuota
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// per altre tessere
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);
}
}Infine, ridefiniamo il metodo paintComponent, derivato dalla classe JPane. Poi utilizziamo il metodo drawGrid, seguito dal metodo drawStartMessage per visualizzare un messaggio che invita a cliccare per avviare il gioco:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Clicca per avviare un nuovo gioco";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// centriamo la stringa s per la tessera data (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);
}Reagiamo alle azioni degli utenti nell'interfaccia utente
Per garantire il corretto svolgimento del gioco, è necessario gestire le azioni degli utenti nell'interfaccia. A questo scopo, aggiungiamo un'implementazione di MouseListener su Jpanel e il codice per spostare i tasselli, già mostrato sopra:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// utilizzato per consentire agli utenti di interagire sulla griglia facendo clic
// è tempo di implementare l'interazione con gli utenti per spostare i tasselli e risolvere il gioco !
if (gameOver) {
newGame();
} else {
// otteniamo la posizione del clic
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// clic sulla griglia ?
if (ex gridSize || ey gridSize)
return;
// otteniamo la posizione nella griglia
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// otteniamo la posizione della cella vuota
int c2 = blankPos % size;
int r2 = blankPos / size;
// convertiamo in coordinata 1D
int clickPos = r1 * size + c1;
int dir = 0;
// cerchiamo la direzione per spostare più tasselli contemporaneamente
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) {
// muoviamo i tasselli nella direzione
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// controlliamo se il gioco è risolto
gameOver = isSolved();
}
// ridisegniamo il pannello
repaint();
}
});Il codice va inserito nel costruttore della classe GameOfFifteen. Alla fine chiamiamo il metodo newGame per iniziare una nuova partita.
Codice completo del gioco
L'ultimo passo, prima di vedere il gioco in azione, è mettere insieme tutti gli elementi del codice. Ecco cosa otterremo:
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;
// Stiamo per creare un Gioco del 15 con Java 8 e Swing
// Se hai domande, sentiti libero di leggere i commenti ;)
public class GameOfFifteen extends JPanel { // la nostra griglia sarà disegnata in un pannello dedicato
// Dimensione della nostra istanza del Gioco del 15
private int size;
// Numero di tessere
private int nbTiles;
// Dimensione UI della griglia
private int dimension;
// Colore di primo piano
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // usiamo un colore arbitrario
// Oggetto Random per mescolare le tessere
private static final Random RANDOM = new Random();
// Memorizzazione delle tessere in un Array 1D di interi
private int[] tiles;
// Dimensione della tessera su UI
private int tileSize;
// Posizione della tessera vuota
private int blankPos;
// Margine per la griglia sul frame
private int margin;
// Dimensione UI della griglia
private int gridSize;
private boolean gameOver; // true se il gioco è finito, false altrimenti
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// inizializza le tessere
nbTiles = size * size - 1; // -1 perché non contiamo la tessera vuota
tiles = new int[size * size];
// calcola la dimensione della griglia e la dimensione della tessera
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) {
// usato per far interagire gli utenti sulla griglia facendo clic
// è tempo di implementare l'interazione con gli utenti per muovere le tessere e risolvere il gioco !
if (gameOver) {
newGame();
} else {
// ottieni la posizione del clic
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// clic nella griglia ?
if (ex gridSize || ey gridSize)
return;
// ottieni la posizione nella griglia
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// ottieni la posizione della cella vuota
int c2 = blankPos % size;
int r2 = blankPos / size;
// convertiamo in coordinate 1D
int clickPos = r1 * size + c1;
int dir = 0;
// cerchiamo direzione per più mosse di tessere insieme
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) {
// muoviamo le tessere nella direzione
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// controlliamo se il gioco è risolto
gameOver = isSolved();
}
// ripaintiamo il pannello
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // reset in stato iniziale
shuffle(); // mescola
} while(!isSolvable()); // fallo finché la griglia non sia risolvibile
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;
}
}
// Solo metà delle permutazioni del puzzle sono risolvibili.
// Ogni volta che una tessera è preceduta da una tessera con un valore più alto conta
// come un'inversione. Nel nostro caso, con la tessera vuota nella posizione risolta,
// il numero di inversioni deve essere pari affinché il puzzle sia risolvibile
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) // se la tessera vuota non è nella posizione risolta => non risolto
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("Gioco del Quindici");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// centra sullo schermo
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Finalmente, giochiamo!
È il momento di avviare il gioco e vederlo in azione. Il campo dovrebbe apparire come segue:

Proviamo a risolvere il puzzle. Se tutto è andato a buon fine, otteniamo questo:

Ecco fatto. Ti aspettavi qualcosa di più? 🙂
Skillbox consiglia:
- Corso pratico .
- Corso online pratico .
- Corso pratico di due anni .
Fonte: habr.com
