
«Viisteist», vĂ”i «Viisteistk» on suurepĂ€rane nĂ€ide lihtsast loogikapallist, mis on populaarne ĂŒle kogu maailma. MĂ”istatuse lahendamiseks tuleb numbrilised ruudud paigutada Ă”igesse jĂ€rjestusse, alates vĂ€iksemast kuni suuremani. See pole lihtne, kuid on huvitav.
KÀesolevas Ôpetuses nÀitame, kuidas arendada «Viisteist» Java 8 ja Eclipse'i abil. Kasutame kasutajaliidese loomiseks Swing API-d.
Tuletame meelde: kĂ”igile «Habr» lugejatele â 10 000 rubla allahindlus igale Skillboxi kursusele, kasutades sooduskoodi «Habr».
Skillbox soovitab: Haridusalane veebikursus .
MĂ€ngu kavandamine
Selles etapis tuleb mÀÀratleda omadused:
- Size â mĂ€nguvĂ€lja suurus;
- nbTiles â ruutude arv mĂ€nguvĂ€ljal. nbTiles = size*size â 1;
- Tiles â viisteist, mis on ĂŒhe mÔÔtmega tĂ€isarvude massiiv. Igal viisteistk on ainulaadne vÀÀrtus vahemikus [0, nbTiles]. Null tĂ€histab tĂŒhja ruutu;
- blankPos â tĂŒhja ruudu positsioon.
MĂ€ngu loogika
Peab mÀÀratlema nullimise (reset) meetodi, mida kasutatakse uue mÀnguasendi initsialiseerimiseks. Nii seadistame vÀÀrtuse iga viisteistk massiivi elemendi jaoks. SeejÀrel paneme blankPos massiivi viimasesse positsiooni.
Vajame ka segamise meetodit, et viisteiste massiivi segada. JĂ€tame tĂŒhja viisteistk segamisprotsessist vĂ€lja, et see jÀÀks oma kohale.
Kuna ainult pool vÔimalikest mÀnguasenditest on lahendatavad, tuleb kontrollida saadud segamise tulemust, et veenduda, et praegune paigutus on tÔesti lahendatav. Selleks mÀÀratleme meetodi isSolvable.
Kui konkreetsele viisteistk'le eelneb viisteistk, mille vÀÀrtus on kĂ”rgem, loetakse seda inversiooniks. Kui tĂŒhja viisteistk on oma kohas, peab inversioonide arv olema paarisarv, et mĂ”istatus oleks lahendatav. Seega loeme inversioonide arvu ja tagastame true, kui arv on paarisarv.
SeejĂ€rel on oluline mÀÀratleda meetod isSolved, et kontrollida, kas meie viisteistk paigutus on lahendatud. Esiteks vaatame, kus asub tĂŒhja viisteistk. Kui algasendis, siis on praegune paigutus uus, mitte kunagi lahendatud. SeejĂ€rel vaatame ĂŒle plaatide vÀÀrtused vastupidises jĂ€rjekorras ning kui viisteistk'i vÀÀrtus erineb vastavast indeksist +1, tagastame false. Vastupidiselt tagastame meetodi lĂ”pus true, kuna mĂ”istatus on juba lahendatud.
Teine meetod, mida tuleb mÀÀratleda, on newGame. Seda vajatakse uue mÀngu eksemplari loomiseks. Selleks lÀhtestame mÀnguvÀlja, segame seda ja jÀtkame, kuni mÀngupositsioon on lahendatav.
Siin on koodinĂ€ide, mis sisaldab peamist loogikat viiekĂŒmnese mĂ€ngu jaoks:
private void newGame() {
do {
reset(); // lÀhtesta algseisund
shuffle(); // sega
} while(!isSolvable()); // jÀtka, kuni ruut on lahendatav
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;
}
}
// Ainult pool puzzle permutatsioonidest on lahendatavad/
// Iga kord, kui plaat on eelnevalt kÔrgema vÀÀrtusega plaadi ees,
// loetakse see pöördeks. Meie puhul, kui tĂŒhi plaat on lahendatud positsioonis,
// peavad pöörded olema paarisarv, et puzzle oleks lahendatav
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) // kui tĂŒhi plaat ei ole lahendatud positsioonis == mitte lahendatud
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}LĂ”puks tuleb programmeerida viiekĂŒmnese mĂ€ngu liikuvad plaadid massiivis. Seda koodi kutsutakse hiljem tagasi (callback), et reageerida kursori liikumisele. Meie mĂ€ng toetab mitme plaadi liikumist samaaegselt. Seega, pĂ€rast selle leidmist, et vajutatud positsioon ekraanil vastab viiekĂŒmnesele plaadile, leiame tĂŒhja plaadi positsiooni ja otsime liikumise suunda, et toetada mitme plaadi liikumist samaaegselt.
Siin on koodinÀide:
// 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;Arendame kasutajaliidese Swing API-l
On aeg tegeleda liidesega. Alustame Jpanel klassist. Siis joonistame mĂ€ngulaudadele viiekĂŒmneseid â iga plaadi suuruse arvutamiseks kasutame andmeid, mis on mÀÀratud mĂ€ngu konstruktoriparameetris:
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;Margin on samuti paramater, mis on mÀÀratud mÀngu konstruktoris.
NĂŒĂŒd tuleb mÀÀratleda meetod drawGrid, et joonistada ruut ja viiekĂŒmnesed ekraanile. AnalĂŒĂŒsime viiekĂŒmneste massiivi ja muundame koordinaadid kasutajaliidese koordinaatideks. SeejĂ€rel joonistame iga viiekĂŒmnese vastava numbri keskele:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// me konverteerime 1D koordinaadid 2D koordinaatideks, arvestades 2D massiivi suurust
int r = i / size;
int c = i % size;
// me konverteerime UI koordinaatideks
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// kontrollime, kas on eriline juhtum tĂŒhja plaadi puhul
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// teiste plaatide puhul
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);
}
}LĂ”puks ĂŒletame paintComponent meetodi, mis pĂ€rineb JPane klassist. Seega kasutame drawGrid meetodit, ning seejĂ€rel drawStartMessage meetodit, et kuvada sĂ”num, mis kutsub klikkima mĂ€ngu alustamiseks:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Kliki, et alustada uut mÀngu";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// tsenterime stringi s antud plaadi (x,y) osas
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);
}Reageerime kasutaja toimingutele UI-s
Kuna mÀng peab oma rada kÀima, on vajalik kasutaja toimingute töötlemine UI-s. Selleks lisame MouseListeneri rakenduse Jpanelile ja koodi, et viia viieplaate, nagu eelnevalt nÀidatud:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// vÔimaldab kasutajatel suhelda ruudustiku kaudu klikkides
// on aeg rakendada kasutaja interaktsiooni plaatide liigutamiseks mÀngu lahendamiseks!
if (gameOver) {
newGame();
} else {
// saadakse klikikoht
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klikiti ruudustikus?
if (ex gridSize || ey gridSize)
return;
// saadakse positsioon ruudustikus
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// saadakse tĂŒhja ruudu positsioon
int c2 = blankPos % size;
int r2 = blankPos / size;
// me konverteerime 1D koordinaati
int clickPos = r1 * size + c1;
int dir = 0;
// otsime suunda mitme plaadi liikumiseks korraga
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) {
// liigutame plaate suunas
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// kontrollime, kas mÀng on lahendatud
gameOver = isSolved();
}
// uuendame paneeli
repaint();
}
});Kood loome klassi GameOfFifteen konstruktoris. LĂ”pus kutsume ĂŒles meetodi newGame, et alustada uut mĂ€ngu.
TÀielik mÀngu kood
Viimane samm enne mÀngu nÀgemist on kokku koguda kÔik koodi elemendid. Siin on, mis meid ootab:
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;
// Loome 15-tĂŒkki mĂ”istatus mĂ€ngu Java 8 ja Swingiga
// Kui teil on kĂŒsimusi, siis looge julgelt kommentaare ;)
public class GameOfFifteen extends JPanel { // meie ruudustik joonistatakse spetsiaalses paneelis
// Meie 15-tĂŒkki mĂ€ngu eksemplari suurus
private int size;
// Plaatide arv
private int nbTiles;
// Ruudustiku UI mÔÔtmed
private int dimension;
// Esimene vÀrv
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // kasutame suvalist vÀrvi
// Plaatide segamiseks suvaline objekt
private static final Random RANDOM = new Random();
// Plaatide salvestamine 1D tÀisarvude massiivi
private int[] tiles;
// Plaadi suurus UI-s
private int tileSize;
// TĂŒhja plaadi asukoht
private int blankPos;
// Ruudustiku servad raamile
private int margin;
// Ruudustiku UI suurus
private int gridSize;
private boolean gameOver; // tÔene, kui mÀng on lÀbi, vale vastasel juhul
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// algatame plaadid
nbTiles = size * size - 1; // -1, sest me ei arvesse tĂŒhja plaati
tiles = new int[size * size];
// arvutame ruudustiku suuruse ja plaadi suuruse
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) {
// kasutame, et lasta kasutajatel ruudustikus klÔpsata
// on aeg rakendada kasutajatega suhtlemist, et liikuda plaate mÀngu lahendamiseks!
if (gameOver) {
newGame();
} else {
// saadud klÔpsu positsioon
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// klÔps ruudustikus?
if (ex gridSize || ey gridSize)
return;
// saadud positsioon ruudustikus
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// saadud tĂŒhja ruumi positsioon
int c2 = blankPos % size;
int r2 = blankPos / size;
// teisendame 1D koordinaatidesse
int clickPos = r1 * size + c1;
int dir = 0;
// otsime suunda, et mitu plaati korraga liikuda
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) {
// liigutame plaate suunas
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// kontrollime, kas mÀng on lahendatud
gameOver = isSolved();
}
// uuendame paneeli
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // lÀhtestamine algseisundisse
shuffle(); // segamine
} while(!isSolvable()); // teeme, kuni ruut on lahendatav
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;
}
}
// Ainult pool mÔistatuse permutatsioonidest on lahendatavad.
// Iga kord, kui plaat eelneb kÔrgema vÀÀrtusega plaadile, siis loetakse
// selleks inversioon. Meie puhul peab lahendatud positsiooniga
// tĂŒhja plaadi puhul olema inversioonide arv paaris, et mĂ”istatus oleks lahendatav
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) // kui tĂŒhi plaat ei ole lahendatud positsioonis ==> pole lahendatud
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("15 Plaadi MĂ€ng");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// keskendame ekraanil
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}LÔpuks mÀngime!
On just Ôige aeg mÀngu kÀivitamiseks ja selle toimimist kontrollimiseks. VÀli peaks vÀlja nÀgema jÀrgmiselt:

PĂŒĂŒame mĂ”istatusi lahendada. Kui kĂ”ik lĂ€heb hĂ€sti, saame sellise tulemuse:

Nii ongi. Kas ootasite rohkem? đ
Skillbox soovitab:
- Praktiline kursus .
- Rakenduslik veebikursus .
- Kaks aastat praktilist kursust .
Allikas: habr.com
