
«Femton», eller «Femtonspel» â ett utmĂ€rkt exempel pĂ„ ett enkelt logiskt spel som Ă€r populĂ€rt över hela vĂ€rlden. För att lösa pusslet mĂ„ste man ordna klossarna med siffror i rĂ€tt ordning, frĂ„n lĂ€gst till högst. Det Ă€r inte lĂ€tt, men intressant.
I dagens handledning visar vi hur man utvecklar «Femton» i Java 8 med Eclipse. För att utveckla anvÀndargrÀnssnittet kommer vi att anvÀnda Swing API.
PĂ„minnelse: för alla lĂ€sare av "Habr" â 10 000 rubel rabatt vid registrering för vilken kurs som helst pĂ„ Skillbox med rabattkoden "Habr".
Skillbox rekommenderar: Utbildningskurs online .
Designa spelet
I detta steg mÄste vi definiera egenskaperna:
- Size â storleken pĂ„ spelplanen;
- nbTiles â antalet pusselbitar pĂ„ spelplanen. nbTiles = size*size â 1;
- Tiles â pusselbiten, som representeras av en endimensionell array av heltal. Varje pusselbit fĂ„r ett unikt vĂ€rde i intervallet [0, nbTiles]. Noll representerar den tomma rutan;
- blankPos â positionen för den tomma rutan.
Spel-logik
Vi mÄste definiera en metod för ÄterstÀllning (reset) som anvÀnds för att initiera en ny spelposition. SÄ vi sÀtter vÀrdet för varje element i arrayen av pusselbitar. Sedan placerar vi blankPos pÄ den sista positionen i arrayen.
Vi behöver ocksÄ en metod för att blanda (shuffle) arrayen av pusselbitar. Vi inkluderar inte den tomma pusselbiten i blandningsprocessen för att behÄlla dess position.
Eftersom endast hÀlften av de möjliga startpositionerna för pusslet har en lösning, mÄste vi kontrollera det resulterande blandandet för att se till att den aktuella uppstÀllningen faktiskt Àr lösbar. För att göra detta definierar vi en metod isSolvable.
Om en specifik pusselbit föregÄs av en bit med ett högre vÀrde betraktas det som en inversion. NÀr den tomma pusselbiten Àr pÄ sin position mÄste antalet inversioner vara jÀmnt för att pusslet ska vara lösbart. SÄ vi rÀknar antalet inversioner och returnerar true om antalet Àr jÀmnt.
DÀrefter Àr det viktigt att definiera metoden isSolved för att kontrollera om vÄr uppstÀllning av Game Of Fifteen Àr löst. Först ser vi var den tomma pusselbiten Àr. Om den Àr pÄ sin ursprungliga plats, Àr den aktuella uppstÀllningen ny och har aldrig lösts tidigare. Sedan gÄr vi igenom plattorna i omvÀnd ordning, och om vÀrdet pÄ pusselbiten avviker frÄn motsvarande index +1 returnerar vi false. Annars, i slutet av metoden, Àr det dags att returnera true, för pusslet Àr redan löst.
En annan metod som behöver definieras Àr newGame. Den krÀvs för att skapa en ny instans av spelet. För detta ÄterstÀller vi spelplanen, blandar den och fortsÀtter tills spelpositionen Àr lösbar.
HÀr Àr ett exempel pÄ koden med nyckellogiken för femtiofem:
private void newGame() {
do {
reset(); // ÄterstÀll i ursprungligt tillstÄnd
shuffle(); // blanda
} while(!isSolvable()); // gör detta tills rutnÀtet Àr lösbart
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;
}
}
// Endast hÀlften av permutationerna av pusslet Àr lösbara/
// Varje gÄng en flis föregÄs av en flis med högre vÀrde rÀknas
// det som en inversion. I vÄrt fall, med den tomma flisen i den lösta positionen,
// mÄste antalet inversioner vara jÀmnt för att pusslet ska vara lösbart
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) // om den tomma flisen inte Àr i den lösta positionen => inte löst
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Slutligen mÄste vi programmera rörelsen av femtiofem i matrisen. Denna kod kommer att anropas senare via en callback för att reagera pÄ markörens rörelse. VÄrt spel kommer att stödja flera flisflyttningar samtidigt. SÄ efter att vi konverterat den tryckta positionen pÄ skÀrmen till en femtiofem fÄr vi positionen för den tomma flisen och letar efter rörelseriktningen för att stödja flera av sina rörelser samtidigt.
HÀr Àr ett exempel pÄ koden:
// 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 utvecklar UI med Swing API
Det Àr dags att ta hand om grÀnssnittet. Först tar vi klassen Jpanel. Sedan ritar vi femtiofem pÄ fÀltet - för att berÀkna storleken pÄ varje kommer vi att anvÀnda data som anges i spelkonstruktorns parameter:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin Àr ocksÄ en parameter som anges i spelkonstruktorn.
Nu mÄste vi definiera metoden drawGrid för att rita nÀtet och femtiofem pÄ skÀrmen. Vi analyserar matrisen av femtiofem och konverterar koordinaterna till anvÀndargrÀnssnittets koordinater. Sedan ritar vi varje femtiofem med motsvarande nummer i mitten:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// vi konverterar 1D-koordinater till 2D-koordinater med hjÀlp av storleken pÄ den 2D-array
int r = i / size;
int c = i % size;
// vi konverterar till koordinater pÄ UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// kontrollera specialfall för tom ruta
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// för andra rutor
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);
}
}Slutligen kommer vi att Ă„sidosĂ€tta paintComponent-metoden, som Ă€r en hĂ€rledd klass av JPane. Vi anvĂ€nder sedan metoden drawGrid, och efter det â metoden drawStartMessage för att visa meddelandet som uppmanar att klicka för att starta spelet:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Klicka för att starta ett nytt spel";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// centrera strÀngen s för den givna rutan (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);
}Reagera pÄ anvÀndarens handlingar i UI
För att spelet ska fungera som det ska, behöver vi hantera anvÀndarens handlingar i UI. För detta lÀgger vi till implementeringen av MouseListener pÄ Jpanel och koden för att flytta plattorna som redan visats ovan:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// anvÀnds för att lÄta anvÀndare interagera med rutan genom att klicka
// nu Àr det dags att implementera interaktion med anvÀndare för att flytta plattor för att lösa spelet!
if (gameOver) {
newGame();
} else {
// hÀmta positionen för klicket
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klick i rutnÀtet?
if (ex gridSize || ey gridSize)
return;
// hÀmta positionen i rutnÀtet
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// hÀmta positionen för den tomma cellen
int c2 = blankPos % size;
int r2 = blankPos / size;
// vi konverterar till 1D-koordinat
int clickPos = r1 * size + c1;
int dir = 0;
// vi söker riktning för flera plattflyttingar pÄ en gÄng
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 flyttar plattorna i riktningen
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// vi kontrollerar om spelet Àr löst
gameOver = isSolved();
}
// vi mÄlar om panelen
repaint();
}
});Koden placeras i klasskonstruktorn GameOfFifteen. I slutet anropar vi metoden newGame för att pÄbörja ett nytt spel.
FullstÀndig kod för spelet
Sista steget innan vi ser spelet i aktion Àr att sammanstÀlla alla kodkomponenter. SÄ hÀr blir det:
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 kommer att skapa ett 15-pussel med Java 8 och Swing
// Om du har nÄgra frÄgor, tveka inte att lÀsa kommentarerna ;)
public class GameOfFifteen extends JPanel { // vÄr ruta kommer att ritas i en dedikerad panel
// Storleken pÄ vÄr Game of Fifteen-instans
private int size;
// Antal brickor
private int nbTiles;
// Grid UI-dimension
private int dimension;
// FörgrundsfÀrg
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // vi anvÀnder en godtycklig fÀrg
// Slumptalsobjekt för att blanda brickorna
private static final Random RANDOM = new Random();
// Lagring av brickorna i en 1D-array av heltal
private int[] tiles;
// Storleken pÄ en bricka i UI
private int tileSize;
// Positionen för den tomma brickan
private int blankPos;
// Marginal för rutan pÄ ramen
private int margin;
// Grid UI-storlek
private int gridSize;
private boolean gameOver; // true om spelet Àr slut, false annars
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// initiera brickor
nbTiles = size * size - 1; // -1 eftersom vi inte rÀknar med den tomma brickan
tiles = new int[size * size];
// berÀkna rutans storlek och brickstorlek
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) {
// anvÀnds för att lÄta anvÀndarna interagera med rutan genom att klicka
// det Àr dags att implementera interaktion med anvÀndarna för att flytta brickor för att lösa spelet!
if (gameOver) {
newGame();
} else {
// hÀmta positionen för klicket
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klick i rutan?
if (ex gridSize || ey gridSize)
return;
// hÀmta position i rutan
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// hÀmta positionen för den tomma cellen
int c2 = blankPos % size;
int r2 = blankPos / size;
// vi omvandlar till 1D-koordinat
int clickPos = r1 * size + c1;
int dir = 0;
// vi söker riktning för flera brickflyttningar pÄ en gÄng
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 flyttar brickor i riktningen
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// vi kontrollerar om spelet Àr löst
gameOver = isSolved();
}
// vi ritar om panelen
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // ÄterstÀll i ursprungligt tillstÄnd
shuffle(); // blanda
} while(!isSolvable()); // gör det tills rutan blir 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;
}
}
// Endast hÀlften av permutationerna av pusslet Àr lösbara.
// NÀr en bricka följs av en bricka med högre vÀrde rÀknas det
// som en inversion. I vÄrt fall, med den tomma brickan i den lösta positionen,
// mÄste antalet inversioner vara jÀmnt för att pusslet ska vara lösbart
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) // om den tomma brickan inte Àr i den lösta positionen => inte 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();
// centrera pÄ skÀrmen
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Ăntligen spelar vi!
Det Àr dags att starta spelet och se hur det fungerar. Spelet ska se ut som följande:

LÄt oss försöka lösa pusslet. Om allt gÄr bra fÄr vi detta:

Och sĂ„ var det klart. FörvĂ€ntade du dig mer? đ
Skillbox rekommenderar:
- Praktisk kurs .
- TillÀmplig onlinekurs .
- TvÄÄrig praktisk kurs .
KĂ€lla: habr.com
