
«Quindici», o «Quindici» — è un ottimo esempio di un semplice gioco di logica, popolare in tutto il mondo. Per risolvere il puzzle, è 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 «Quindici» in Java 8 con Eclipse. Per lo sviluppo dell'interfaccia utente utilizzeremo l'API Swing.
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 quindici nel campo. nbTiles = size*size — 1;
- Tiles — il quindici, che è un array monodimensionale di numeri interi. Ognuno dei quindici avrà un valore unico nell'intervallo [0, nbTiles]. Lo zero rappresenta il quadrato vuoto;
- blankPos — posizione del quadrato vuoto.
Logica del gioco
È necessario definire un metodo di reset, utilizzato per inizializzare una nuova posizione di gioco. In questo modo impostiamo il valore per ogni elemento dell'array dei quindici. E poi poniamo blankPos nell'ultima posizione dell'array.
È necessario anche un metodo shuffle per mescolare l'array delle tessere. Non includiamo la tessera vuota nel processo di mescolamento, in modo da mantenerla nella sua posizione originale.
Poiché solo metà delle possibili posizioni iniziali del puzzle ha una soluzione, dobbiamo controllare il risultato del mescolamento per assicurarci che la configurazione attuale sia risolvibile. A tal fine, definiamo il metodo isSolvable.
Se una tessera specifica è preceduta da una tessera con un valore più alto, ciò si considera un'inversione. Quando la tessera vuota è al suo posto, il numero di inversioni deve essere pari affinché il puzzle sia risolvibile. Pertanto, contiamo il numero di inversioni e restituiamo true se il numero è pari.
È quindi importante definire il metodo isSolved per verificare se il nostro layout del Gioco delle Quindici è risolto. Iniziamo a controllare dove si trova il tassello vuoto. Se è nella posizione iniziale, allora l'attuale layout è nuovo e non è stato risolto in precedenza. Dopodiché, esaminiamo i tasselli in ordine inverso e, se il valore del tassello è diverso dall'indice corrispondente +1, restituiamo false. Altrimenti, alla fine del metodo possiamo restituire true, poiché il puzzle è già risolto.
Un altro metodo da definire è newGame. Questo è necessario per creare una nuova istanza del gioco. A tal fine, resettiamo il campo da gioco, poi lo mescoliamo e continuiamo finché la posizione del gioco non è risolvibile.
Ecco un esempio di codice con la logica chiave delle 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 dei tasselli in un array. Questo codice verrà richiamato in seguito tramite un callback, per rispondere al movimento del cursore. Il nostro gioco supporterà diversi spostamenti dei tasselli contemporaneamente. Quindi, dopo aver convertito la posizione premuta sullo schermo in un tassello, otteniamo la posizione del tassello vuoto 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
È il momento di lavorare sull'interfaccia. Iniziamo prendendo la classe Jpanel. Poi disegniamo i tasselli sul campo: per calcolare le dimensioni di ognuno utilizzeremo i dati forniti nel parametro del costruttore del gioco:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Il margine è anch'esso un parametro fornito nel costruttore del gioco.
Ora dobbiamo definire il metodo drawGrid per disegnare la griglia e i tasselli sullo schermo. Analizziamo l'array dei tasselli e convertiamo le coordinate in coordinate dell'interfaccia utente. Successivamente, disegniamo ogni tassello con il numero appropriato 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 le coordinate nell'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 le 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, sovrascriviamo il metodo paintComponent, che deriva dalla classe JPane. Poi utilizziamo il metodo drawGrid e successivamente il 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 iniziare 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) {
// centra 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 dell'utente nell'interfaccia utente
Affinché il gioco proceda senza intoppi, è necessario gestire le azioni dell'utente nell'interfaccia utente. Per questo aggiungiamo l'implementazione di MouseListener a Jpanel e il codice per muovere i tasselli, già mostrato sopra:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// utilizzato per consentire agli utenti di interagire con la griglia facendo clic
// è tempo di implementare l'interazione con gli utenti per spostare 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 posizione nella griglia
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// ottieni posizione della cella vuota
int c2 = blankPos % size;
int r2 = blankPos / size;
// convertiamo nella coordinata 1D
int clickPos = r1 * size + c1;
int dir = 0;
// cerchiamo la direzione per muovere più tessere 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 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();
}
// riprendiamo 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, è unire 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 creando il 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); // utilizziamo 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 nell'interfaccia
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 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 consentire agli utenti di interagire con la griglia cliccando
// è ora di implementare l'interazione con gli utenti per spostare 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 posizione nella griglia
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// ottieni posizione della cella vuota
int c2 = blankPos % size;
int r2 = blankPos / size;
// convertiamo in coord 1D
int clickPos = r1 * size + c1;
int dir = 0;
// cerchiamo la direzione per più spostamenti 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 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();
}
// ridisegniamo il pannello
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // resettare allo stato iniziale
shuffle(); // mescolare
} while(!isSolvable()); // fai 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 la metà delle permutazioni del puzzle sono risolvibili.
// Ogni volta che una tessera è preceduta da una tessera con valore più alto conta
// come un'inversione. Nel nostro caso, con la tessera vuota nella posizione risolta,
// il numero delle 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 risolta
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();
// centra sullo schermo
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Finalmente, giochiamo!
È il momento giusto per avviare il gioco e vederlo all'opera. Il campo dovrebbe apparire come segue:

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

Ecco fatto. Vi aspettavate di più? 🙂
Skillbox consiglia:
- Corso pratico .
- Corso online applicativo .
- Corso pratico di due anni .
Fonte: habr.com
