
"Pesëmbëdhjetë", ose "Pesëmbëdhjetëka" është një shembull i shkëlqyer i një loje logjike të thjeshtë, e njohur në të gjithë botën. Për të zgjidhur enigmat, është e nevojshme të vendosen katrorët me numra në rend, nga më i vogli në më të madhin. Nuk është e thjeshtë, por është interesante.
Në këtë tutorial po tregojmë se si të zhvillojmë "Pesëmbëdhjetë" në Java 8 me Eclipse. Për zhvillimin e UI do të përdorim Swing API.
Kujtojmë: për të gjithë lexuesit e «Habra» — zbritje prej 10,000 rublesh për regjistrimin në çdo kurs Skillbox me kodin promovues «Habr».
Skillbox rekomandon: Kurs online edukativ .
Projektimi i lojës
Në këtë fazë duhet të përcaktojmë pronat:
- Size — madhësia e fushës së lojës;
- nbTiles — numri i pesëmbëdhjetëve në fushë. nbTiles = size*size — 1;
- Tiles — pesëmbëdhjete, e cila përfaqëson një varg një dimensionale të numrave të plotë. Çdo pesëmbëdhjetë do të marrë një vlerë unike brenda gamës [0, nbTiles]. Nula përfaqëson katrorin bosh;
- blankPos — pozita e katrorit bosh.
Logjika e lojës
Duhet të përcaktojmë metodën reset, e cila përdoret për të inicializuar një pozicion të ri në lojë. Kështu vendosim vlerën për çdo element të vargut të pesëmbëdhjetëve. Pastaj vendosim blankPos në pozitat e fundit të vargut.
Përveç kësaj, duhet një metodë shuffle për të përzier vargun e pesëmbëdhjetëve. Ne nuk e përfshijmë pesëmbëdhjetën bosh në procesin e përzierjes, për të mbajtur atë në pozitat e saj të mëparshme.
Duke qenë se vetëm gjysma e pozitat fillestare të mundshme të enigmes ka një zgjidhje, duhet të kontrollojmë rezultatin e përzierjes për të siguruar se ngarkesa aktuale është e zgjidhshme. Për ta bërë këtë, përcaktojmë metodën isSolvable.
Nëse një pesëmbëdhjete e caktuar paraprihet nga një pesëmbëdhjete me një vlerë më të lartë, kjo konsiderohet një inversion. Kur pesëmbëdhjeta bosh është në vendin e saj, numri i inversioneve duhet të jetë çift për të bërë enigmat të zgjidhshme. Pra, ne numërojmë numrin e inversioneve dhe kthejmë true nëse numri është çift.
Pastaj është e rëndësishme të përcaktojmë metodën isSolved, për të kontrolluar nëse ngarkesa jonë e Game Of Fifteen është zgjidhur. Para se gjithash, shohim se ku ndodhet pesëmbëdhjeta bosh. Nëse është në pozita fillestare, atëherë ngarkesa aktuale është një e re, e pa zgjidhur më parë. Pastaj kalojmë përmes pllakatave në rendin e kundërt, dhe, nëse vlera e pesëmbëdhjetës ndryshon nga indeksi përkatës +1, kthejmë false. Në të kundërt, në fund të metodës është koha të kthejmë true, sepse enigmat tashmë janë zgjidhur.
Një metodë tjetër që duhet të definoni është newGame. Kjo kërkohet për të krijuar një instancë të re të lojës. Për këtë, ne rikthejmë fushën e lojës, pastaj e përzjejmë atë dhe vazhdojmë derisa pozita e lojës të jetë e zgjidhshme.
Ja një shembull kodi me logjikën kyçe të puzzle-it më të madh.
private void newGame() {
do {
reset(); // rikthe në gjendjen fillestare
shuffle(); // përzjej
} while(!isSolvable()); // bëje derisa grilja të jetë e zgjidhshme
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;
}
}
// Vetëm gjysma e permutimeve të puzzles janë të zgjidhshme/
// Sa herë që një pllakë i paraprin një pllakë me vlerë më të lartë, kjo llogaritet
// si një inversim. Në rastin tonë, me pllakën bosh në pozita të zgjidhura,
// numri i inversimeve duhet të jetë çift për puzzle-n që të jetë e zgjidhshme
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) // nëse plaka bosh nuk është në pozita të zgjidhura ==>
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}Më në fund, duhet të programojmë lëvizjen e pllakatave në array. Ky kod do të thirret më vonë përmes një thirrjeje (callback) për të reaguar ndaj lëvizjes së kursorit. Loja jonë do të mbështesë disa lëvizje të pllakatave njëkohësisht. Kështu, pasi ta kemi konvertuar pozita e prekur në ekran në një pllakë, ne marrim pozita e pllakatës bosh dhe kërkojmë drejtimin e lëvizjes për të mbështetur disa lëvizje të saj njëkohësisht.
Ja një shembull kodi:
// 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;Po zhvillojmë UI me Swing API
Është koha për të punuar mbi ndërfaqen. Fillimisht marim klasën Jpanel. Pastaj do të vizatojmë pllakatat në fushë - për të llogaritur dimensionet e secilës do të shfrytëzojmë të dhënat e caktuara në parametrin e konstruktuesit të lojës:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin gjithashtu është një parametër i caktuar në konstruktuesin e lojës.
Tani duhet të përcaktojmë metodën drawGrid për të vizatuar gridin dhe pllakatat në ekran. Anlizojmë array-n e pllakatave dhe i konvertojmë koordinatat në koordinatat e ndërfaqes. Pastaj do të vizatojmë secilën pllakë me numrin përkatës në qendër:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// ne convertojmë koordinatat 1D në koordinata 2D duke pasur parasysh madhësinë e Array 2D
int r = i / size;
int c = i % size;
// ne konvertojmë në koordinatat në UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// kontrollojmë rastin special për pllakën bosh
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// për pllakat e tjera
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);
}
}Në fund, do të tejkaluam metodën paintComponent, e cila është një klasë e nxjerrë nga JPane. Më pas do të përdorim metodën drawGrid, dhe pas saj metodën drawStartMessage për të shfaqur mesazhin që sugjeron të klikoni për të filluar lojën:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Kliko për të filluar lojën e re";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// qendron string s për pllaken e dhënë (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);
}Reagojmë ndaj veprimeve të përdoruesit në UI
Për të siguruar që loja ecën normalisht, është e nevojshme të trajtojme veprimet e përdoruesit në UI. Për këtë, shtojmë implementimin e MouseListener në Jpanel dhe kodin për të lëvizur pllakat, siç është treguar më lart:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// përdoret për të lejuar përdoruesit të interaktojnë në grid duke klikuar
// tani është koha të implementojmë interaksionin me përdoruesit për të lëvizur pllakat për të zgjidhur lojën!
if (gameOver) {
newGame();
} else {
// merr pozita e klikimit
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// kliko në grid?
if (ex gridSize || ey gridSize)
return;
// merr poziten në grid
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// merr pozitën e qelizës bosh
int c2 = blankPos % size;
int r2 = blankPos / size;
// ne konvertojmë në koordinatën 1D
int clickPos = r1 * size + c1;
int dir = 0;
// kërkojmë drejtimin për lëvizje shumëplakjesh njëherazi
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) {
// ne lëvizim pllakat në drejtimin e duhur
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// kontrollojmë nëse loja është zgjidhur
gameOver = isSolved();
}
// ne ripikturojmë panelin
repaint();
}
});Kodi e vendosim në konstruktorin e klasës GameOfFifteen. Në fund të fundit, thërrasim metodën newGame për të filluar një lojë të re.
Kodi i plotë i lojës
Hapi i fundit, para se të shihni lojën në veprim, është të bashkoni të gjithë elementet e kodit. Këto janë rezultatet:
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;
// Ne do të krijojmë një lojë Puzzle 15 me Java 8 dhe Swing
// Nëse keni ndonjë pyetje, mos hezitoni të lexoni komentet ;)
public class GameOfFifteen extends JPanel { // gridi ynë do të vizatohet në një Panel të dedikuar
// Madhësia e instancës sonë të Lojës së Pesëmbëdhjetë
private int size;
// Numri i pllakatave
private int nbTiles;
// Dimensioni i UI të Grid-it
private int dimension;
// Ngjyra e Parë
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // ne përdorim një ngjyrë arbitrare
// Objekt i rastësishëm për të përzier pllakat
private static final Random RANDOM = new Random();
// Ruajtja e pllakatave në një Array 1D të integerëve
private int[] tiles;
// Madhësia e pllakës në UI
private int tileSize;
// Pozita e pllakës bosh
private int blankPos;
// Margin për gridin në frame
private int margin;
// Madhësia e UI të Grid-it
private int gridSize;
private boolean gameOver; // e vërtetë nëse loja ka përfunduar, e falsë për ndryshe
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// init pllakat
nbTiles = size * size - 1; // -1 sepse nuk e numërojmë pllakën bosh
tiles = new int[size * size];
// llogarit madhësinë e grid-it dhe madhësinë e pllakave
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) {
// përdoret për të lejuar përdoruesit të ndërveprojnë në grid duke klikuar
// është koha të implementojmë ndërveprimin me përdoruesit për të lëvizur pllakat për të zgjidhur lojën!
if (gameOver) {
newGame();
} else {
// merr pozitat e klikimit
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klikimi në grid?
if (ex gridSize || ey gridSize)
return;
// merr pozitat në grid
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// merr pozitat e qelizës bosh
int c2 = blankPos % size;
int r2 = blankPos / size;
// ne konvertojmë në koordinatat 1D
int clickPos = r1 * size + c1;
int dir = 0;
// ne kërkojmë drejtimin për lëvizje të shumëfishta të pllakatave
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) {
// ne lëvizim pllakatat në drejtim
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// ne kontrollojmë nëse loja është zgjidhur
gameOver = isSolved();
}
// ne ripikturojmë panelin
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // rivendos në gjendjen fillestare
shuffle(); // përzihet
} while(!isSolvable()); // bëj deri sa grid duhet të jetë i zgjidhshëm
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;
}
}
// Vetëm gjysma e permutimeve të puzzles janë të zgjidhshme.
// Sa herë që një pllakë paraprihet nga një pllakë me vlerë më të lartë, ajo llogaritet
// si një inversion. Në rastin tonë, me pllakën bosh në pozitat e zgjidhura,
// numri i inversioneve duhet të jetë çift për të qenë puzzle i zgjidhshëm
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) // nëse plaka bosh nuk është në pozitat e zgjidhura => nuk është e zgjidhur
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("Lojë e Pesëmbëdhjetë");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// centroni në ekran
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}Më në fund, luajmë!
Është koha për të nisur lojën dhe për ta provuar atë në veprim. Fusha duhet të duket si më poshtë:

Të provosh të zgjidhesh enigmat. Nëse gjithçka shkoi mirë, ne marrim këtë:

Kjo është gjithçka. A prisnit më shumë? 🙂
Skillbox rekomandon:
- Kurs praktik .
- Kurs online aplikativ .
- Kursi praktik dyvjeçar .
Burimi: habr.com
