Java-da "Tag" - tam hüquqlu bir oyunu necə inkişaf etdirmək olar
"On beş" və ya "on beş" bütün dünyada məşhur olan sadə məntiq oyununun əla nümunəsidir. Tapmacanı həll etmək üçün kiçikdən böyüyə qədər nömrələri olan kvadratları sıralamalısınız. Bu asan deyil, amma maraqlıdır.
Bugünkü dərslikdə biz sizə Eclipse ilə Java 8-də On Fifteen-i necə inkişaf etdirəcəyinizi göstərəcəyik. UI-ni inkişaf etdirmək üçün Swing API-dən istifadə edəcəyik.
Xatırladırıq:"Habr" ın bütün oxucuları üçün - "Habr" promosyon kodundan istifadə edərək hər hansı bir Skillbox kursuna yazılarkən 10 000 rubl endirim.
Fayans tam ədədlərin birölçülü massivi olan etiketdir. Teqlərin hər biri [0, nbTiles] diapazonunda unikal dəyər alacaq. Sıfır boş kvadratı göstərir;
blankPos — boş kvadratın mövqeyi.
Oyun məntiqi
Yeni oyun mövqeyini işə salmaq üçün istifadə edilən sıfırlama metodunu müəyyən etməliyik. Bu yolla biz tags massivinin hər bir elementi üçün dəyər təyin edirik. Yaxşı, sonra massivin son mövqeyində blankPos yerləşdiririk.
Teqlər massivini qarışdırmaq üçün bizə həmçinin qarışdırma üsulu lazımdır. Boş etiketi eyni vəziyyətdə buraxmaq üçün qarışdırma prosesinə daxil etmirik.
Tapmacanın mümkün başlanğıc mövqelərinin yalnız yarısının həlli olduğundan, hazırkı tərtibatın hətta həll oluna biləcəyinə əmin olmaq üçün əldə edilən qarışdırma nəticəsini yoxlamaq lazımdır. Bunun üçün isSolvable metodunu təyin edirik.
Müəyyən bir teqdən əvvəl daha yüksək qiymətə malik bir teq varsa, o, inversiya hesab olunur. Boş yer yerində olduqda, tapmacanın həll edilə bilməsi üçün inversiyaların sayı bərabər olmalıdır. Beləliklə, biz inversiyaların sayını hesablayırıq və rəqəm cüt olarsa, həqiqəti qaytarırıq.
Daha sonra Game Of Fifteen layoutumuzun həll olunub-olunmadığını yoxlamaq üçün isSolved metodunu müəyyən etmək vacibdir. Əvvəlcə boş yerin harada olduğuna baxırıq. Başlanğıc vəziyyətdədirsə, cari düzülmə əvvəllər qərar verilməmiş yenidir. Sonra plitələr arasında tərs qaydada təkrarlayırıq və etiketin dəyəri müvafiq indeks +1-dən fərqlidirsə, yalanı qaytarırıq. Əks təqdirdə, metodun sonunda həqiqətə qayıtmaq vaxtıdır, çünki tapmaca artıq həll edilmişdir.
Müəyyən edilməli olan başqa bir üsul newGame-dir. Oyunun yeni nümunəsini yaratmaq tələb olunur. Bunu etmək üçün biz oyun sahəsini sıfırlayırıq, sonra qarışdırırıq və oyun mövqeyi həll olunana qədər davam edirik.
Budur etiketin əsas məntiqi ilə kod nümunəsi:
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;
}
Nəhayət, massivdəki etiketlərin hərəkətini proqramlaşdırmalısınız. Bu kod kursorun hərəkətinə cavab vermək üçün daha sonra geri zəng vasitəsilə çağırılacaq. Oyunumuz eyni anda birdən çox kafel hərəkətini dəstəkləyəcək. Beləliklə, biz ekranda sıxılmış vəziyyəti teqə çevirdikdən sonra boş teqin mövqeyini alırıq və onun bir neçə hərəkətini eyni vaxtda dəstəkləmək üçün hərəkət istiqamətini axtarırıq.
Budur bir kod nümunəsi:
// 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;
Swing API istifadə edərək UI inkişaf etdiririk
İnterfeys üzərində işləmək vaxtıdır. Əvvəlcə Jpanel sinifini alırıq. Sonra sahədə etiketlər çəkirik - hər birinin ölçülərini hesablamaq üçün oyun konstruktoru parametrində göstərilən məlumatlardan istifadə edəcəyik:
Margin həm də oyun konstruktorunda müəyyən edilmiş bir parametrdir.
İndi ekranda tor və ləkələri çəkmək üçün drawGrid metodunu təyin etməliyik. Teqlər massivini təhlil edirik və koordinatları istifadəçi interfeysi koordinatlarına çeviririk. Sonra hər bir nöqtəni mərkəzdə müvafiq nömrə ilə çəkin:
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);
}
}
Nəhayət, JPane sinfindən əldə edilən paintComponent metodunu ləğv edək. Biz daha sonra oyuna başlamaq üçün klikləməyimizi xahiş edən mesajı göstərmək üçün drawGrid metodundan, ardınca drawStartMessage metodundan istifadə edirik:
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-də istifadəçi hərəkətlərinə reaksiya
Oyunun öz kursunu davam etdirməsi üçün istifadəçi interfeysində istifadəçi hərəkətlərini emal etmək lazımdır. Bunu etmək üçün, Jpanel-də MouseListener tətbiqini və yuxarıda göstərilən ləkələrin hərəkət kodunu əlavə edin:
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();
}
});
Kodu GameOfFifteen sinfinin konstruktorunda yerləşdiririk. Ən sonunda yeni oyuna başlamaq üçün newGame metodunu çağırırıq.
Tam oyun kodu
Oyunu hərəkətdə görməzdən əvvəl son addım bütün kod elementlərini bir araya gətirməkdir. Nə baş verir:
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);
});
}
}
Nəhayət, gəlin oynayaq!
Oyunu işə salmağın və onu hərəkətdə sınamağın vaxtı gəldi. Sahə belə görünməlidir:
Gəlin tapmacanı həll etməyə çalışaq. Hər şey qaydasındadırsa, bunu alırıq: