"Piecpadsmit" vai "Piecpadsmit" ir lielisks piemÄrs vienkÄrÅ”ai loÄ£ikas spÄlei, kas ir populÄra visÄ pasaulÄ. Lai atrisinÄtu mÄ«klu, jums ir jÄsakÄrto kvadrÄti ar cipariem, sÄkot no mazÄkÄ lÄ«dz lielÄkajam. Tas nav viegli, bet ir interesanti.
Å odienas apmÄcÄ«bÄ mÄs parÄdÄm, kÄ izstrÄdÄt Fifteen Java 8, izmantojot Eclipse. Lai izstrÄdÄtu lietotÄja interfeisu, mÄs izmantosim Swing API.
nbTiles ā tagu skaits laukÄ. nbTiles = izmÄrs*izmÄrs - 1;
FlÄ«zes ir tags, kas ir viendimensijas veselu skaitļu masÄ«vs. Katrs no tagiem saÅems unikÄlu vÄrtÄ«bu diapazonÄ [0, nbTiles]. Nulle norÄda tukÅ”u kvadrÄtu;
blankPos ā tukÅ”Ä kvadrÄta pozÄ«cija.
SpÄles loÄ£ika
Mums ir jÄdefinÄ atiestatÄ«Å”anas metode, ko izmanto, lai inicializÄtu jaunu spÄles pozÄ«ciju. TÄdÄ veidÄ mÄs iestatÄm vÄrtÄ«bu katram tagu masÄ«va elementam. Tad mÄs ievietojam blankPos masÄ«va pÄdÄjÄ pozÄ«cijÄ.
Mums ir nepiecieÅ”ama arÄ« jaukÅ”anas metode, lai sajauktu tagu masÄ«vu. MÄs neiekļaujam tukÅ”o tagu jaukÅ”anas procesÄ, lai atstÄtu to tajÄ paÅ”Ä pozÄ«cijÄ.
TÄ kÄ tikai pusei no iespÄjamÄm mÄ«klas sÄkuma pozÄ«cijÄm ir risinÄjums, jums ir jÄpÄrbauda iegÅ«tais jaukÅ”anas rezultÄts, lai pÄrliecinÄtos, ka paÅ”reizÄjais izkÄrtojums ir vienmÄrÄ«gi atrisinÄms. Lai to izdarÄ«tu, mÄs definÄjam metodi isSolvaable.
Ja pirms noteikta taga ir tags ar lielÄku vÄrtÄ«bu, tas tiek uzskatÄ«ts par inversiju. Kad tukÅ”Ä vieta ir vietÄ, inversiju skaitam jÄbÅ«t vienÄdam, lai mÄ«kla bÅ«tu atrisinÄma. TÄtad mÄs saskaitÄm inversiju skaitu un atgriežam patiesu, ja skaitlis ir pÄra.
PÄc tam ir svarÄ«gi definÄt isSolved metodi, lai pÄrbaudÄ«tu, vai mÅ«su piecpadsmito spÄļu kombinÄcija ir atrisinÄta. Vispirms skatÄmies, kur ir tukÅ”a vieta. Ja sÄkotnÄjÄ pozÄ«cijÄ, tad paÅ”reizÄjais izlÄ«dzinÄjums ir jauns, iepriekÅ” nav noteikts. PÄc tam mÄs atkÄrtojam elementus apgrieztÄ secÄ«bÄ, un, ja atzÄ«mes vÄrtÄ«ba atŔķiras no atbilstoÅ”Ä indeksa +1, tiek atgriezta kļūda. PretÄjÄ gadÄ«jumÄ metodes beigÄs ir pienÄcis laiks atgriezt patieso, jo mÄ«kla jau ir atrisinÄta.
VÄl viena metode, kas jÄdefinÄ, ir newGame. Ir nepiecieÅ”ams izveidot jaunu spÄles gadÄ«jumu. Lai to izdarÄ«tu, mÄs atiestatÄm spÄles laukumu, pÄc tam sajaucam to un turpinÄm, lÄ«dz spÄles pozÄ«cija ir atrisinÄma.
Å eit ir koda piemÄrs ar taga atslÄgas loÄ£iku:
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;
}
Visbeidzot, jums ir jÄieprogrammÄ tagu kustÄ«ba masÄ«vÄ. Å is kods tiks izsaukts vÄlÄk, izmantojot atzvanÄ«Å”anu, lai reaÄ£Ätu uz kursora kustÄ«bu. MÅ«su spÄle atbalstÄ«s vairÄkas flīžu kustÄ«bas vienlaikus. TÄdÄjÄdi pÄc tam, kad esam pÄrveidojuÅ”i nospiesto pozÄ«ciju uz ekrÄna par tagu, mÄs iegÅ«stam tukÅ”Äs atzÄ«mes pozÄ«ciju un meklÄjam kustÄ«bas virzienu, lai vienlaikus atbalstÄ«tu vairÄkas tÄs kustÄ«bas.
Å eit ir koda piemÄrs:
// 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;
MÄs izstrÄdÄjam lietotÄja saskarni, izmantojot Swing API
Ir pienÄcis laiks strÄdÄt pie interfeisa. Vispirms mÄs Åemam Jpanel klasi. PÄc tam uz lauka uzzÄ«mÄjam tagus - lai aprÄÄ·inÄtu katra izmÄrus, izmantosim spÄles konstruktora parametrÄ norÄdÄ«tos datus:
Marža ir arÄ« parametru komplekts spÄles konstruktorÄ.
Tagad mums ir jÄdefinÄ drawGrid metode, lai zÄ«mÄtu režģi un plankumus uz ekrÄna. MÄs analizÄjam tagu masÄ«vu un pÄrvÄrÅ”am koordinÄtas lietotÄja interfeisa koordinÄtÄs. PÄc tam uzzÄ«mÄjiet katru vietu ar atbilstoÅ”o numuru centrÄ:
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);
}
}
Visbeidzot, ignorÄsim metodi paintComponent, kas izriet no JPane klases. PÄc tam mÄs izmantojam metodi drawGrid, kam seko metodi drawStartMessage, lai parÄdÄ«tu ziÅojumu, kas aicina noklikŔķinÄt, lai sÄktu spÄli:
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);
}
ReaÄ£Ä uz lietotÄja darbÄ«bÄm lietotÄja saskarnÄ
Lai spÄle darbotos savu gaitu, ir nepiecieÅ”ams apstrÄdÄt lietotÄja darbÄ«bas UI. Lai to izdarÄ«tu, Jpanel pievienojiet MouseListener implementÄciju un vietu pÄrvietoÅ”anas kodu, kas jau parÄdÄ«ts iepriekÅ”:
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();
}
});
MÄs ievietojam kodu GameOfFifteen klases konstruktorÄ. PaÅ”Äs beigÄs mÄs izsaucam newGame metodi, lai sÄktu jaunu spÄli.
Pilns spÄles kods
PÄdÄjais solis, pirms redzÄt spÄli darbÄ«bÄ, ir visu koda elementu salikÅ”ana. LÅ«k, kas notiek:
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);
});
}
}
Beidzot spÄlÄsim!
Ir pienÄcis laiks palaist spÄli un pÄrbaudÄ«t to darbÄ«bÄ. Laukam vajadzÄtu izskatÄ«ties Å”Ädi:
MÄÄ£inÄsim atrisinÄt mÄ«klu. Ja viss noritÄja labi, mÄs iegÅ«stam Å”o: