"Pesëmbëdhjetë" ose "Pesëmbëdhjetë" është një shembull i shkëlqyer i një loje të thjeshtë logjike që është e njohur në të gjithë botën. Për të zgjidhur enigmën, ju duhet t'i rregulloni sheshet me numra në rregull, nga më i vogli tek më i madhi. Nuk është e lehtë, por është interesante.
Në tutorialin e sotëm do t'ju tregojmë se si të zhvilloni Fifteen në Java 8 me Eclipse. Për të zhvilluar UI, ne do të përdorim Swing API.
Kujtojmë:për të gjithë lexuesit e "Habr" - një zbritje prej 10 rubla kur regjistroheni në çdo kurs Skillbox duke përdorur kodin promovues "Habr".
nbTiles — numri i etiketave në fushë. nbTiles = madhësia*madhësia - 1;
Pllakat është një etiketë që është një grup njëdimensional i numrave të plotë. Secila prej etiketave do të marrë një vlerë unike në rangun [0, nbTiles]. Zero tregon një katror bosh;
blankPos - pozicioni i katrorit bosh.
Logjika e lojës
Ne duhet të përcaktojmë një metodë rivendosjeje të përdorur për të inicializuar një pozicion të ri loje. Në këtë mënyrë vendosim një vlerë për çdo element të grupit të etiketave. Epo, atëherë vendosim blankPos në pozicionin e fundit të grupit.
Ne gjithashtu kemi nevojë për një metodë të përzierjes për të përzier grupin e etiketave. Ne nuk e përfshijmë etiketën boshe në procesin e përzierjes në mënyrë që ta lëmë në të njëjtin pozicion.
Meqenëse vetëm gjysma e pozicioneve të mundshme fillestare të enigmës kanë një zgjidhje, ju duhet të kontrolloni rezultatin e përzierjes që rezulton për t'u siguruar që paraqitja aktuale është e zgjidhshme. Për ta bërë këtë, ne përcaktojmë metodën isSolvable.
Nëse një etiketë e caktuar paraprihet nga një etiketë me një vlerë më të lartë, ai konsiderohet një përmbysje. Kur vendi bosh është në vend, numri i përmbysjeve duhet të jetë i barabartë që enigma të jetë e zgjidhshme. Pra, ne numërojmë numrin e përmbysjeve dhe kthejmë të vërtetën nëse numri është çift.
Më pas është e rëndësishme të përcaktoni metodën isSolved për të kontrolluar nëse paraqitja jonë e Game Of Fifteen është zgjidhur. Së pari ne shikojmë se ku është vendi bosh. Nëse në pozicionin fillestar, atëherë shtrirja aktuale është e re, jo e vendosur më parë. Më pas ne përsërisim nëpër pllaka në rend të kundërt, dhe nëse vlera e etiketës është e ndryshme nga indeksi përkatës +1, ne kthejmë false. Përndryshe, në fund të metodës është koha për t'u kthyer e vërtetë sepse enigma tashmë është zgjidhur.
Një metodë tjetër që duhet të përcaktohet është newGame. Kërkohet të krijohet një shembull i ri i lojës. Për ta bërë këtë, ne rivendosim fushën e lojës, më pas e përziejmë atë dhe vazhdojmë derisa pozicioni i lojës të jetë i zgjidhshëm.
Këtu është një kod shembull me logjikën kryesore të etiketës:
private void newGame() {
do {
reset(); // reset in initial state
shuffle(); // shuffle
} while(!isSolvable()); // make it until grid be solvable
gameOver = false;
}
private void reset() {
for (int i = 0; i < tiles.length; i++) {
tiles[i] = (i + 1) % tiles.length;
}
// we set blank cell at the last
blankPos = tiles.length - 1;
}
private void shuffle() {
// don't include the blank tile in the shuffle, leave in the solved position
int n = nbTiles;
while (n > 1) {
int r = RANDOM.nextInt(n--);
int tmp = tiles[r];
tiles[r] = tiles[n];
tiles[n] = tmp;
}
}
// Only half permutations of the puzzle are solvable/
// Whenever a tile is preceded by a tile with higher value it counts
// as an inversion. In our case, with the blank tile in the solved position,
// the number of inversions must be even for the puzzle to be solvable
private boolean isSolvable() {
int countInversions = 0;
for (int i = 0; i < nbTiles; i++) {
for (int j = 0; j < i; j++) {
if (tiles[j] > tiles[i])
countInversions++;
}
}
return countInversions % 2 == 0;
}
private boolean isSolved() {
if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}
Së fundi, ju duhet të programoni lëvizjen e etiketave në grup. Ky kod do të thirret më vonë nëpërmjet një thirrjeje për t'iu përgjigjur lëvizjes së kursorit. Loja jonë do të mbështesë lëvizje të shumta të pllakave në të njëjtën kohë. Kështu, pasi të kemi kthyer pozicionin e shtypur në ekran në një etiketë, marrim pozicionin e etiketës bosh dhe kërkojmë një drejtim lëvizjeje për të mbështetur disa nga lëvizjet e tij në të njëjtën kohë.
Këtu është një kod shembull:
// 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;
Ne zhvillojmë UI duke përdorur Swing API
Është koha për të punuar në ndërfaqen. Së pari marrim klasën Jpanel. Pastaj ne tërheqim etiketat në fushë - për të llogaritur madhësitë e secilës, ne do të përdorim të dhënat e specifikuara në parametrin e konstruktorit të lojës:
Marzhi është gjithashtu një parametër i vendosur në konstruktorin e lojës.
Tani duhet të përcaktojmë metodën drawGrid për të vizatuar rrjetin dhe pikat në ekran. Ne analizojmë grupin e etiketave dhe i konvertojmë koordinatat në koordinatat e ndërfaqes së përdoruesit. Pastaj vizatoni secilën pikë me numrin përkatës në qendër:
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);
}
}
Së fundi, le të anashkalojmë metodën paintComponent, e cila rrjedh nga klasa JPane. Më pas përdorim metodën drawGrid, e ndjekur nga metoda drawStartMessage për të shfaqur një mesazh që na kërkon të klikojmë 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 = "Click to start new game";
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);
}
Reagimi ndaj veprimeve të përdoruesit në UI
Në mënyrë që loja të vazhdojë rrjedhën e saj, është e nevojshme të përpunohen veprimet e përdoruesit në UI. Për ta bërë këtë, shtoni zbatimin e MouseListener në Jpanel dhe kodin për lëvizjen e pikave, të treguar tashmë më lart:
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 < 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;
}
// we check if game is solved
gameOver = isSolved();
}
// we repaint panel
repaint();
}
});
E vendosim kodin në konstruktorin e klasës GameOffFifteen. Në fund, ne thërrasim metodën newGame për të filluar një lojë të re.
Kodi i plotë i lojës
Hapi i fundit përpara se të shikoni lojën në veprim është të vendosni të gjithë elementët e kodit së bashku. Ja çfarë ndodh:
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 are going to create a Game of 15 Puzzle with Java 8 and Swing
// If you have some questions, feel free to read comments ;)
public class GameOfFifteen extends JPanel { // our grid will be drawn in a dedicated Panel
// Size of our Game of Fifteen instance
private int size;
// Number of tiles
private int nbTiles;
// Grid UI Dimension
private int dimension;
// Foreground Color
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // we use arbitrary color
// Random object to shuffle tiles
private static final Random RANDOM = new Random();
// Storing the tiles in a 1D Array of integers
private int[] tiles;
// Size of tile on UI
private int tileSize;
// Position of the blank tile
private int blankPos;
// Margin for the grid on the frame
private int margin;
// Grid UI Size
private int gridSize;
private boolean gameOver; // true if game over, false otherwise
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// init tiles
nbTiles = size * size - 1; // -1 because we don't count blank tile
tiles = new int[size * size];
// calculate grid size and tile size
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) {
// 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 < 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;
}
// we check if game is solved
gameOver = isSolved();
}
// we repaint panel
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // reset in intial state
shuffle(); // shuffle
} while(!isSolvable()); // make it until grid be solvable
gameOver = false;
}
private void reset() {
for (int i = 0; i < tiles.length; i++) {
tiles[i] = (i + 1) % tiles.length;
}
// we set blank cell at the last
blankPos = tiles.length - 1;
}
private void shuffle() {
// don't include the blank tile in the shuffle, leave in the solved position
int n = nbTiles;
while (n > 1) {
int r = RANDOM.nextInt(n--);
int tmp = tiles[r];
tiles[r] = tiles[n];
tiles[n] = tmp;
}
}
// Only half permutations of the puzzle are solvable.
// Whenever a tile is preceded by a tile with higher value it counts
// as an inversion. In our case, with the blank tile in the solved position,
// the number of inversions must be even for the puzzle to be solvable
private boolean isSolvable() {
int countInversions = 0;
for (int i = 0; i < nbTiles; i++) {
for (int j = 0; j < i; j++) {
if (tiles[j] > tiles[i])
countInversions++;
}
}
return countInversions % 2 == 0;
}
private boolean isSolved() {
if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved
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 < 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);
}
}
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Click to start new game";
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);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
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 on the screen
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
Më në fund, le të luajmë!
Është koha për të nisur lojën dhe për ta provuar atë në veprim. Fusha duhet të duket si kjo:
Le të përpiqemi të zgjidhim enigmën. Nëse gjithçka shkoi mirë, marrim këtë: