"Tag" dalam Java - bagaimana untuk membangunkan permainan yang lengkap
"Lima belas" atau "Lima belas" ialah contoh terbaik permainan logik mudah yang popular di seluruh dunia. Untuk menyelesaikan teka-teki, anda perlu menyusun petak dengan nombor mengikut urutan, dari terkecil hingga terbesar. Ia tidak mudah, tetapi ia menarik.
Dalam tutorial hari ini kami akan menunjukkan kepada anda cara membangunkan Fifteen dalam Java 8 dengan Eclipse. Untuk membangunkan UI kami akan menggunakan API Swing.
Kami mengingatkan:untuk semua pembaca "Habr" - diskaun sebanyak 10 rubel apabila mendaftar dalam mana-mana kursus Skillbox menggunakan kod promosi "Habr".
nbTiles — bilangan tag dalam medan. nbJubin = saiz*saiz - 1;
Jubin ialah teg yang merupakan tatasusunan satu dimensi integer. Setiap teg akan menerima nilai unik dalam julat [0, nbTiles]. Sifar menunjukkan petak kosong;
blankPos — kedudukan petak kosong.
Logik permainan
Kita perlu menentukan kaedah tetapan semula yang digunakan untuk memulakan kedudukan permainan baharu. Dengan cara ini kami menetapkan nilai untuk setiap elemen tatasusunan tag. Nah, kemudian kita letakkan blankPos di kedudukan terakhir tatasusunan.
Kami juga memerlukan kaedah shuffle untuk mengocok tatasusunan tag. Kami tidak memasukkan teg kosong dalam proses mengocok untuk membiarkannya dalam kedudukan yang sama.
Memandangkan hanya separuh daripada kemungkinan kedudukan permulaan teka-teki mempunyai penyelesaian, anda perlu menyemak hasil shuffle yang terhasil untuk memastikan bahawa susun atur semasa boleh diselesaikan. Untuk melakukan ini, kami mentakrifkan kaedah isSolvable.
Jika teg tertentu didahului oleh teg dengan nilai yang lebih tinggi, ia dianggap sebagai penyongsangan. Apabila tempat kosong berada di tempatnya, bilangan penyongsangan mestilah genap untuk teka-teki itu boleh diselesaikan. Jadi kita mengira bilangan penyongsangan dan kembalikan benar jika nombor itu genap.
Maka adalah penting untuk menentukan kaedah isSolved untuk menyemak sama ada susun atur Game Of Fifteen kami telah diselesaikan. Mula-mula kita lihat di mana tempat kosong itu. Jika dalam kedudukan awal, maka penjajaran semasa adalah baharu, tidak diputuskan sebelum ini. Kami kemudian melelang melalui jubin dalam susunan terbalik, dan jika nilai teg berbeza daripada indeks +1 yang sepadan, kami mengembalikan palsu. Jika tidak, pada akhir kaedah sudah tiba masanya untuk kembali benar kerana teka-teki telah diselesaikan.
Kaedah lain yang perlu ditakrifkan ialah newGame. Ia diperlukan untuk mencipta contoh baru permainan. Untuk melakukan ini, kami menetapkan semula medan permainan, kemudian kocok dan teruskan sehingga kedudukan bermain boleh diselesaikan.
Berikut ialah contoh kod dengan logik utama teg:
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;
}
Akhir sekali, anda perlu memprogramkan pergerakan tag dalam tatasusunan. Kod ini akan dipanggil kemudian melalui panggilan balik untuk bertindak balas kepada pergerakan kursor. Permainan kami akan menyokong berbilang pergerakan jubin pada masa yang sama. Oleh itu, selepas kami menukar kedudukan yang ditekan pada skrin menjadi tag, kami mendapat kedudukan tag kosong dan mencari arah pergerakan untuk menyokong beberapa pergerakannya pada masa yang sama.
Berikut ialah contoh kod:
// 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;
Kami membangunkan UI menggunakan Swing API
Sudah tiba masanya untuk bekerja pada antara muka. Mula-mula kita ambil kelas Jpanel. Kemudian kami melukis tag pada medan - untuk mengira saiz setiap satu, kami akan menggunakan data yang ditentukan dalam parameter pembina permainan:
Margin juga merupakan parameter yang ditetapkan dalam pembina permainan.
Sekarang kita perlu menentukan kaedah drawGrid untuk melukis grid dan bintik-bintik pada skrin. Kami menganalisis tatasusunan tag dan menukar koordinat kepada koordinat antara muka pengguna. Kemudian lukis setiap tempat dengan nombor yang sepadan di tengah:
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);
}
}
Akhir sekali, mari kita ganti kaedah paintComponent, yang berasal daripada kelas JPane. Kami kemudian menggunakan kaedah drawGrid, diikuti dengan kaedah drawStartMessage untuk memaparkan mesej yang menggesa kami mengklik untuk memulakan permainan:
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);
}
Bertindak balas terhadap tindakan pengguna dalam UI
Agar permainan berjalan lancar, tindakan pengguna perlu diproses dalam UI. Untuk melakukan ini, tambahkan pelaksanaan MouseListener pada Jpanel dan kod untuk tempat bergerak, yang telah ditunjukkan di atas:
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();
}
});
Kami meletakkan kod dalam pembina kelas GameOfFifteen. Pada penghujungnya, kami memanggil kaedah NewGame untuk memulakan permainan baharu.
Kod permainan penuh
Langkah terakhir sebelum melihat permainan beraksi ialah meletakkan semua elemen kod bersama-sama. Inilah yang berlaku:
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);
});
}
}
Akhirnya, mari kita bermain!
Sudah tiba masanya untuk melancarkan permainan dan mengujinya dalam tindakan. Medan sepatutnya kelihatan seperti ini:
Mari cuba selesaikan teka-teki. Jika semuanya berjalan lancar, kami mendapat ini:
Itu sahaja. Adakah anda menjangkakan lebih banyak lagi? 🙂