
«Femten», eller «Fifteen» — et glimrende eksempel på et simpelt logisk spil, der er populært over hele verden. For at løse puslespillet skal man arrangere firkanterne med tal i rækkefølge fra lavest til højest. Det er ikke let, men det er interessant.
I dagens tutorial viser vi, hvordan man udvikler «Fifteen» i Java 8 med Eclipse. Vi vil bruge Swing API til at udvikle UI.
Vi minder om: til alle læsere af 'Habr' — rabat på 10.000 rubler ved tilmelding til et hvilket som helst Skillbox-kursus med rabatkoden 'Habr'.
Skillbox anbefaler: Online uddannelseskursus .
Spildesign
I dette trin skal vi definere egenskaberne:
- Size — størrelsen på spillefeltet;
- nbTiles — antallet af femtens i feltet. nbTiles = size * size — 1;
- Tiles — femten, der er et endimensionelt array af heltal. Hver af femtens vil få en unik værdi i intervallet [0, nbTiles]. Nul angiver det tomme felt;
- blankPos — positionen for det tomme felt.
Spillets logik
Det er nødvendigt at definere en reset-metode, der bruges til at initialisere en ny spilleposition. Her sætter vi værdien for hvert element i femten-arrayet. Derefter placerer vi blankPos i den sidste position i arrayet.
Derudover er der brug for en shuffle-metode til at blande femten-arrayet. Vi inkluderer ikke den tomme femten i blandingsprocessen for at holde den i sin nuværende position.
Da kun halvdelen af de mulige startpositioner for puslespillet er løsbare, skal vi kontrollere det resulterende blandingsresultat for at sikre, at den aktuelle opstilling overhovedet er løsbar. For at gøre dette definerer vi en isSolvable-metode.
Hvis en bestemt femten har en femten med en højere værdi foran sig, betragtes det som en inversion. Når den tomme femten er på sin plads, skal antallet af inversioner være lige for at puslespillet kan være løsbar. Så vi tæller antallet af inversioner og returnerer true, hvis tallet er lige.
Derefter er det vigtigt at definere en isSolved-metode for at kontrollere, om vores opstilling af Game Of Fifteen er løst. Først ser vi, hvor den tomme femten er. Hvis den er i startpositionen, er den aktuelle opstilling ny og ikke tidligere løst. derefter gennemgår vi fliserne i omvendt rækkefølge, og hvis værdien af femten er forskellig fra den tilsvarende indeks +1, returnerer vi false. Ellers, i slutningen af metoden, er det tid til at returnere true, fordi puslespillet allerede er løst.
Endnu en metode, der skal defineres, er newGame. Den er nødvendig for at oprette et nyt spilindhold. Til dette nulstiller vi spillefeltet, derefter blander vi det og fortsætter, indtil spilpositionen er løsbar.
Her er et kodeeksempel med nøglelogikken for femt eller titern:
private void newGame() {
do {
reset(); // nulstille til starten
shuffle(); // blande
} while(!isSolvable()); // fortsæt indtil rutinen er løsbar
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;
}
}
// Kun halvdelen af permutationerne af puslespillet er løsbare/
// Når en flise efterfølges af en flise med højere værdi, tæller det
// som en inversion. I vores tilfælde, med den blanke flise i den løste position,
// skal antallet af inversioner være lige for at puslespillet kan være løsbar
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) // hvis den blanke flise ikke er i den løste position => ikke løst
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Endelig skal bevægelsen af femt eller titern programmeres i arrayet. Denne kode vil blive kaldt senere via en callback for at reagere på cursorbevægelser. Vores spil vil understøtte flere placeringer af fliser samtidig. Så efter at have konverteret den trykkede position på skærmen til en femt eller titern, finder vi positionen af den blanke flise og søger efter bevægelsesretningen for at understøtte flere bevægelser samtidigt.
Her er et kodeeksempel:
// 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;Vi udvikler UI med Swing API
Det er tid til at arbejde på brugerfladen. Først tager vi klassen Jpanel. Derefter tegner vi fliserne på feltet - for at beregne størrelsen af hver vil vi bruge data givet i konstructorens parametre:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin er også en parameter givet i konstructoren.
Nu skal metoden drawGrid defineres for at tegne rutenettet og fliserne på skærmen. Vi analyserer arrayet af fliser og konverterer koordinaterne til UI-koordinater. Derefter tegner vi hver flise med det relevante nummer i midten:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// vi konverterer 1D koordinater til 2D koordinater givet størrelsen på 2D Array
int r = i / size;
int c = i % size;
// vi konverterer til koordinater i UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// tjekker specialtilfælde for blank flise
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// for andre fliser
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);
}
}Endelig skal vi redefinere metoden paintComponent, som er en afledt metode fra klassen JPane. Derefter bruger vi metoden drawGrid, og efterfølgende — metoden drawStartMessage til at vise en besked, der opfordrer til at klikke for at starte spillet:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Klik for at starte et nyt spil";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// centrerer strengen s til den givne flise (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);
}Reagerer på brugerhandlinger i UI
For at sikre at spillet fortsætter, er det nødvendigt at håndtere brugerhandlinger i UI. Derfor tilføjer vi implementeringen af MouseListener på JPanel og koden til at flytte fliserne, som allerede er vist ovenfor:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// bruges til at lade brugerne interagere med rutenet ved at klikke
// det er tid til at implementere interaktion med brugerne for at flytte fliser for at løse spillet!
if (gameOver) {
newGame();
} else {
// få positionen for klikket
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klikker i rutenettet?
if (ex gridSize || ey gridSize)
return;
// få position i rutenettet
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// få positionen af den tomme celle
int c2 = blankPos % size;
int r2 = blankPos / size;
// vi konverterer til 1D koordinat
int clickPos = r1 * size + c1;
int dir = 0;
// vi søger retning for flere flisemoves ad gangen
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) {
// vi flytter fliser i retningen
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// vi tjekker om spillet er løst
gameOver = isSolved();
}
// vi maler panelet igen
repaint();
}
});Koden placeres i klassen GameOfFifteen's konstruktor. Til sidst kalder vi metoden newGame for at starte et nyt spil.
Den fulde kode til spillet
Det sidste skridt, før du ser spillet i aktion, er at samle alle kodeelementerne sammen. Her er hvad der kommer ud:
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;
// Vi skal lave et 15-spils spil med Java 8 og Swing
// Hvis du har spørgsmål, er du velkommen til at læse kommentarerne ;)
public class GameOfFifteen extends JPanel { // vores gitter vil blive tegnet i en dedikeret panel
// Størrelsen af vores Game of Fifteen instans
private int size;
// Antal fliser
private int nbTiles;
// Grid UI Dimension
private int dimension;
// Forgrundsfarve
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // vi bruger en vilkårlig farve
// Random objekt til at blande fliser
private static final Random RANDOM = new Random();
// Opbevaring af fliser i et 1D array af heltal
private int[] tiles;
// Størrelse af fliser i UI
private int tileSize;
// Position af den blanke flise
private int blankPos;
// Margin for gitteret på rammen
private int margin;
// Grid UI Størrelse
private int gridSize;
private boolean gameOver; // sandt hvis spillet er slut, falsk ellers
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// init fliser
nbTiles = size * size - 1; // -1 fordi vi ikke tæller den blanke flise
tiles = new int[size * size];
// beregn gitterstørrelse og flisestørrelse
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) {
// bruges til at lade brugerne interagere med gitteret ved at klikke
// det er tid til at implementere interaktionen med brugerne for at flytte fliserne og løse spillet!
if (gameOver) {
newGame();
} else {
// få positionen af klikket
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klik i gitteret ?
if (ex gridSize || ey gridSize)
return;
// få positionen i gitteret
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// få positionen af den blanke celle
int c2 = blankPos % size;
int r2 = blankPos / size;
// vi konverterer til 1D koordinater
int clickPos = r1 * size + c1;
int dir = 0;
// vi søger retning for flere flisebevægelser på én gang
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) {
// vi flytter fliserne i retningen
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// vi tjekker om spillet er løst
gameOver = isSolved();
}
// vi maler panelet igen
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // nulstil til initial tilstand
shuffle(); // bland
} while(!isSolvable()); // gør det, indtil gitteret er løseligt
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;
}
}
// Kun halvdelen af permutationerne af puslespillet er løselige.
// Hver gang en flise er forudgået af en flise med højere værdi, tæller det
// som en inversion. I vores tilfælde, med den blanke flise i løst position,
// skal antallet af inversioner være lige for puslespillet at være løseligt
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) // hvis den blanke flise ikke er i den løste position => ikke løst
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 på skærmen
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Endelig, lad os spille!
Det er tid til at starte spillet og se det i aktion. Spillefeltet skal se således ud:

Lad os forsøge at løse puzzlen. Hvis alt gik som planlagt, får vi dette:

Det var det hele. Forventede du mere? 🙂
Skillbox anbefaler:
- Praktisk kursus .
- Anvendt online-kursus .
- Toårigt praktisk kursus .
Kilde: habr.com
