
«Vijftien», of «Vijftien-Spel» is een uitstekend voorbeeld van een eenvoudig logisch spel, dat wereldwijd populair is. Om de puzzel op te lossen, moeten de vierkanten met cijfers in volgorde worden geplaatst, van klein naar groot. Dit is niet eenvoudig, maar wel interessant.
In deze tutorial laten we zien hoe je «Vijftien» kunt ontwikkelen in Java 8 met Eclipse. Voor de ontwikkeling van de UI gebruiken we de Swing API.
Ter herinnering: voor alle lezers van «Habr» â een korting van 10.000 roebel bij inschrijving voor elke cursus van Skillbox met de promocode «Habr».
Skillbox raadt aan: Online opleidingscursus .
Spelontwerp
In deze stap moeten we de eigenschappen bepalen:
- Size â de afmeting van het speelveld;
- nbTiles â het aantal vijftien tegels op het veld. nbTiles = size*size â 1;
- Tiles â de vijftien tegel, die een eendimensionale array van gehele getallen vertegenwoordigt. Elke vijftien tegels krijgt een unieke waarde in het bereik [0, nbTiles]. Nul geeft een leeg vierkant aan;
- blankPos â de positie van het lege vierkant.
Spelmechanica
We moeten een reset-methode definiëren, die wordt gebruikt om een nieuwe speelpositie te initialiseren. Op deze manier stellen we een waarde in voor elk element van de vijftien array. Vervolgens plaatsen we blankPos op de laatste positie van de array.
We hebben ook een shuffle-methode nodig om de vijftien array te schudden. We sluiten de lege vijftien uit van het schudproces om deze op zijn plaats te houden.
Aangezien slechts de helft van de mogelijke startposities van de puzzel oplosbaar is, moeten we het resultaat van de schudroutine controleren om te verifiëren of de huidige opstelling oplosbaar is. Om dit te doen, definiëren we een isSolvable-methode.
Als er een vijftien tegel met een hogere waarde voorafgaat aan een specifieke vijftien tile, wordt dit als een inversie beschouwd. Wanneer de lege vijftien op zijn plaats is, moet het aantal inversies even zijn om de puzzel oplosbaar te maken. We tellen het aantal inversies en retourneren true als het aantal even is.
Vervolgens is het belangrijk een isSolved-methode te definiëren om te controleren of onze Game Of Fifteen opstelling opgelost is. Eerst kijken we waar de lege vijftien zich bevindt. Als het in de startpositie is, is de huidige opstelling nieuw en eerder niet opgelost. Vervolgens doorlopen we de tegels in omgekeerde volgorde, en als de waarde van de vijftien afwijkt van de bijbehorende index +1, retourneren we false. In andere gevallen, aan het einde van de methode, is het tijd om true terug te geven, omdat de puzzel al opgelost is.
Een andere methode die moet worden gedefinieerd, is newGame. Deze is nodig om een nieuw spelinstantie te creëren. Hiervoor resetten we het speelveld, schudden het en gaan door totdat de spelsituatie oplosbaar is.
Hier is een voorbeeld van de code met de kernlogica van de puzzel:
private void newGame() {
do {
reset(); // reset naar de initiële staat
shuffle(); // schudden
} while(!isSolvable()); // blijf doorgaan totdat het rooster oplosbaar is
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;
}
}
// Slechts de helft van de permutaties van de puzzel zijn oplosbaar/
// Telkens wanneer een tegel wordt voorafgegaan door een tegel met een hogere waarde, telt dit
// als een inversie. In ons geval, met de lege tegel in de opgeloste positie,
// moet het aantal inversies even zijn voor de puzzel oplosbaar is
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) // als de lege tegel niet in de opgeloste positie is ==> niet opgelost
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Ten slotte moeten we de beweging van de puzzelstukjes in de array programmeren. Deze code zal later worden aangeroepen via een callback om te reageren op de beweging van de cursor. Onze game zal meerdere tegelverschuivingen tegelijkertijd ondersteunen. Dus nadat we de aangeraakte positie op het scherm naar een puzzelstukje hebben geconverteerd, krijgen we de positie van de lege tegel en zoeken we de bewegingsrichting om meerdere verplaatsingen tegelijkertijd te ondersteunen.
Hier is een voorbeeld van de code:
// 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;UI ontwikkelen met Swing API
Het is tijd om aan de interface te werken. Eerst nemen we de klasse Jpanel. Vervolgens tekenen we de puzzelstukjes op het veld - voor het berekenen van de afmetingen van elk stukje maken we gebruik van de gegevens die zijn opgegeven in de constructorparameters van het spel:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin is ook een parameter die is opgegeven in de constructor van het spel.
Nu moeten we de methode drawGrid definiëren voor het tekenen van het rooster en de puzzelstukjes op het scherm. We analyseren de array van puzzelstukjes en converteren de coördinaten naar gebruikersinterface-coördinaten. Vervolgens tekenen we elk puzzelstukje met het bijbehorende nummer in het midden:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// we convert 1D coords to 2D coords given the size of the 2D Array
int r = i / size;
int c = i % size;
// we convert in coords on the UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// check special case for blank tile
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// for other tiles
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);
}
}Laten we de paintComponent-methode overrulen, die afgeleid is van de JPane-klasse. Vervolgens gebruiken we de drawGrid-methode en daarna de drawStartMessage-methode om het bericht weer te geven dat vraagt om te klikken om het spel te starten:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Klik om een nieuw spel te starten";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// center string s for the given tile (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);
}We reageren op gebruikersacties in de UI
Om ervoor te zorgen dat het spel soepel verloopt, moeten we gebruikersacties in de UI verwerken. Hiervoor voegen we een implementatie van MouseListener toe aan Jpanel en de code voor het verplaatsen van de tegels, zoals eerder getoond:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// used to let users to interact on the grid by clicking
// it's time to implement interaction with users to move tiles to solve the game !
if (gameOver) {
newGame();
} else {
// get position of the click
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// click in the grid ?
if (ex gridSize || 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;
}
// we check if game is solved
gameOver = isSolved();
}
// we repaint panel
repaint();
}
});Plaats de code in de class GameOfFifteen constructor. Roep aan het einde de method newGame aan om een nieuw spel te starten.
Volledige code van het spel
De laatste stap voordat je het spel in actie ziet, is om alle code-elementen samen te voegen. Dit is wat je krijgt:
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;
// We gaan een 15 Puzzel Spel maken met Java 8 en Swing
// Als je vragen hebt, voel je vrij om de opmerkingen te lezen ;)
public class GameOfFifteen extends JPanel { // ons grid wordt getekend in een aparte panel
// Grootte van onze Game of Fifteen instantie
private int size;
// Aantal tegels
private int nbTiles;
// Grid UI Dimensie
private int dimension;
// Voorkeurskleur
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // we gebruiken een willekeurige kleur
// Willekeurige object om tegels te schudden
private static final Random RANDOM = new Random();
// Tegels opslaan in een 1D Array van gehele getallen
private int[] tiles;
// Grootte van tegel op UI
private int tileSize;
// Positie van de lege tegel
private int blankPos;
// Marges voor het grid op het frame
private int margin;
// Grid UI Grootte
private int gridSize;
private boolean gameOver; // waar als spel is afgelopen, onwaar anders
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// init tegels
nbTiles = size * size - 1; // -1 omdat we de lege tegel niet tellen
tiles = new int[size * size];
// bereken grid grootte en tegelgrootte
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) {
// gebruikt om gebruikers op het grid te laten interageren door te klikken
// het is tijd om interactie met gebruikers te implementeren om tegels te verplaatsen om het spel op te lossen !
if (gameOver) {
newGame();
} else {
// krijg de positie van de klik
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klik in het grid ?
if (ex gridSize || ey gridSize)
return;
// krijg positie in het grid
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// krijg positie van de lege cel
int c2 = blankPos % size;
int r2 = blankPos / size;
// we converteren naar de 1D coördinaten
int clickPos = r1 * size + c1;
int dir = 0;
// we zoeken richting voor meerdere tegelbewegingen tegelijk
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 verplaatsen tegels in de richting
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// we controleren of het spel is opgelost
gameOver = isSolved();
}
// we schilderen panel opnieuw
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // reset naar initiële staat
shuffle(); // schudden
} while(!isSolvable()); // maken totdat het grid oplosbaar is
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;
}
}
// Slechts de helft van de permutaties van de puzzel zijn oplosbaar.
// Wanneer een tegel wordt voorafgegaan door een tegel met een hogere waarde, telt het
// als een inversie. In ons geval, met de lege tegel in de opgeloste positie,
// moet het aantal inversies even zijn voor de puzzel oplosbaar te zijn
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) // als de lege tegel niet in de opgeloste positie is ==> niet opgelost
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();
// center op het scherm
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Eindelijk spelen we!
Het is tijd om het spel te starten en het in actie te testen. Het veld zou er als volgt uit moeten zien:

Laten we proberen de puzzel op te lossen. Als alles goed gaat, krijgen we dit:

Dat is alles. Had je meer verwacht? đ
Skillbox raadt aan:
- Praktische cursus .
- Toegepaste online cursus .
- Praktische tweejarenopleiding .
Bron: habr.com
