«Տասնհինգ» կամ «Տասնհինգ» պարզ տրամաբանական խաղի հիանալի օրինակ է, որը տարածված է ամբողջ աշխարհում: Փազլը լուծելու համար հարկավոր է թվերով քառակուսիները դասավորել ամենափոքրից մինչև ամենամեծը: Դա հեշտ չէ, բայց հետաքրքիր է:
Այսօրվա ձեռնարկում մենք ձեզ ցույց կտանք, թե ինչպես զարգացնել Fifteen-ը Java 8-ում Eclipse-ով: UI մշակելու համար մենք կօգտագործենք Swing API-ը:
Հիշեցում.«Habr»-ի բոլոր ընթերցողների համար՝ 10 ռուբլի զեղչ «Habr» գովազդային կոդով Skillbox-ի ցանկացած դասընթացին գրանցվելիս:
Tiles-ը պիտակ է, որն իրենից ներկայացնում է ամբողջ թվերի միաչափ զանգված: Թեգերից յուրաքանչյուրը կստանա եզակի արժեք [0, nbTiles] միջակայքում: Զրոն ցույց է տալիս դատարկ քառակուսի;
blankPos - դատարկ քառակուսու դիրք:
Խաղի տրամաբանություն
Մենք պետք է սահմանենք վերակայման մեթոդ, որն օգտագործվում է խաղի նոր դիրքը սկզբնավորելու համար: Այս կերպ մենք արժեք ենք սահմանում պիտակների զանգվածի յուրաքանչյուր տարրի համար: Դե, ապա մենք տեղադրում ենք blankPos զանգվածի վերջին դիրքում:
Մեզ պետք է նաև խառնել մեթոդ՝ պիտակների զանգվածը խառնելու համար: Մենք դատարկ պիտակը չենք ներառում խառնման գործընթացում՝ այն նույն դիրքում թողնելու համար:
Քանի որ փազլի հնարավոր մեկնարկային դիրքերի միայն կեսն ունի լուծում, դուք պետք է ստուգեք ստացված խառնաշփոթի արդյունքը, որպեսզի համոզվեք, որ ընթացիկ դասավորությունը նույնիսկ լուծելի է: Դա անելու համար մենք սահմանում ենք isSolvable մեթոդը:
Եթե որոշակի պիտակի նախորդում է ավելի բարձր արժեք ունեցող պիտակ, ապա այն համարվում է ինվերսիա: Երբ դատարկ տեղը տեղում է, շրջումների քանակը պետք է հավասար լինի, որպեսզի հանելուկը լուծելի լինի: Այսպիսով, մենք հաշվում ենք ինվերսիաների քանակը և վերադարձնում ճշմարիտ, եթե թիվը զույգ է:
Այնուհետև կարևոր է սահմանել isSolved մեթոդը՝ ստուգելու, թե արդյոք մեր Game Of Fifteen դասավորությունը լուծված է: Սկզբում մենք նայում ենք, թե որտեղ է դատարկ տեղը: Եթե սկզբնական դիրքում է, ապա ներկայիս հավասարեցումը նոր է, նախապես որոշված չէ: Այնուհետև մենք կրկնում ենք սալիկների միջով հակառակ հերթականությամբ, և եթե պիտակի արժեքը տարբերվում է համապատասխան +1 ինդեքսից, մենք վերադարձնում ենք false: Հակառակ դեպքում, մեթոդի վերջում ժամանակն է վերադառնալ ճշմարիտ, քանի որ հանելուկն արդեն լուծված է:
Մեկ այլ մեթոդ, որը պետք է սահմանվի, newGame-ն է։ Պահանջվում է ստեղծել խաղի նոր օրինակ: Դա անելու համար մենք զրոյացնում ենք խաղադաշտը, այնուհետև խառնում ենք այն և շարունակում մինչև խաղային դիրքը լուծելի:
Ահա պիտակի հիմնական տրամաբանությամբ ծածկագրի օրինակ.
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;
}
Վերջապես, դուք պետք է ծրագրավորեք պիտակների շարժումը զանգվածում: Այս կոդը կկանչվի ավելի ուշ՝ հետադարձ զանգի միջոցով՝ կուրսորի շարժմանը արձագանքելու համար: Մեր խաղը կաջակցի մի քանի սալիկների շարժումներ միաժամանակ: Այսպիսով, այն բանից հետո, երբ մենք էկրանի վրա սեղմված դիրքը վերածեցինք պիտակի, մենք ստանում ենք դատարկ պիտակի դիրքը և փնտրում շարժման ուղղություն, որը կաջակցի դրա մի քանի շարժումներին միաժամանակ:
Ահա կոդի օրինակ.
// 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;
Մենք մշակում ենք UI՝ օգտագործելով Swing API
Ժամանակն է աշխատել ինտերֆեյսի վրա: Նախ վերցնում ենք Jpanel դասը։ Այնուհետև դաշտում նկարում ենք պիտակներ. յուրաքանչյուրի չափերը հաշվարկելու համար մենք կօգտագործենք խաղի կոնստրուկտորի պարամետրում նշված տվյալները.
Margin-ը նաև պարամետր է, որը սահմանված է խաղի կոնստրուկտորում:
Այժմ մենք պետք է սահմանենք drawGrid մեթոդը՝ ցանցը և բծերը էկրանին նկարելու համար: Մենք վերլուծում ենք պիտակների զանգվածը և կոորդինատները փոխակերպում օգտագործողի միջերեսի կոորդինատների: Այնուհետև յուրաքանչյուր կետը գծեք կենտրոնում համապատասխան թվով.
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);
}
}
Վերջապես, եկեք անտեսենք paintComponent մեթոդը, որը բխում է JPane դասից։ Այնուհետև մենք օգտագործում ենք drawGrid մեթոդը, որին հաջորդում է drawStartMessage մեթոդը՝ ցուցադրելու հաղորդագրություն, որը հուշում է մեզ սեղմել խաղը սկսելու համար.
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);
}
Արձագանքելով UI-ում օգտագործողի գործողություններին
Որպեսզի խաղն իր ընթացքն ունենա, անհրաժեշտ է մշակել օգտվողի գործողությունները UI-ում: Դա անելու համար ավելացրեք MouseListener-ի ներդրումը Jpanel-ում և տեղաշարժվող կետերի կոդը, որն արդեն ցուցադրված է վերևում.
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();
}
});
Կոդը տեղադրում ենք GameOfFifteen դասի կոնստրուկտորում։ Վերջում մենք կանչում ենք newGame մեթոդը՝ նոր խաղ սկսելու համար։
Ամբողջական խաղի կոդը
Խաղը գործողության մեջ տեսնելուց առաջ վերջին քայլը կոդերի բոլոր տարրերը միասին դնելն է: Ահա թե ինչ է տեղի ունենում.
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);
});
}
}
Վերջապես, եկեք խաղանք:
Ժամանակն է սկսել խաղը և փորձարկել այն գործողության մեջ: Դաշտը պետք է այսպիսի տեսք ունենա.
Փորձենք լուծել հանելուկը. Եթե ամեն ինչ լավ ընթանա, մենք ստանում ենք հետևյալը.