"Lima belas" di Jawa - cara mengembangkan game yang lengkap



Lima belas, atau Lima Belas, adalah contoh yang bagus dari permainan logika sederhana yang populer di seluruh dunia. Untuk memecahkan teka-teki, Anda perlu mengatur kotak dengan angka secara berurutan, dari yang lebih kecil ke yang lebih besar. Ini tidak mudah, tetapi menarik.

Dalam tutorial hari ini, kami menunjukkan bagaimana mengembangkan Lima Belas di Java 8 dengan Eclipse. Untuk mengembangkan UI, kami akan menggunakan Swing API.

Kami mengingatkan Anda: untuk semua pembaca "Habr" - diskon 10.000 rubel saat mendaftar untuk kursus Skillbox apa pun menggunakan kode promo "Habr".

Skillbox merekomendasikan: Kursus pendidikan online "Pengembang Java Profesi" .

Desain game


Pada tahap ini, Anda perlu mendefinisikan properti:
  • Ukuran - ukuran lapangan bermain;
  • nbTiles - jumlah tempat di lapangan. nbTiles = size * size - 1;
  • Ubin adalah tag, yang merupakan array satu dimensi bilangan bulat. Setiap tag akan menerima nilai unik dalam rentang [0, nbTiles]. Nol menunjukkan kotak kosong;
  • blankPos - posisi kotak kosong.

Logika game


Anda perlu menentukan metode reset yang digunakan untuk menginisialisasi posisi permainan baru. Jadi kami menetapkan nilai untuk setiap elemen dari array tag. Baiklah, lalu kita letakkan blankPos di posisi terakhir array.

Anda juga memerlukan metode shuffle untuk mengocok array tag. Kami tidak menyertakan tag kosong dalam proses pengocokan untuk membiarkannya pada posisi semula.

Karena hanya setengah dari posisi awal yang mungkin dari puzzle memiliki solusi, Anda perlu memeriksa hasil pencampuran yang dihasilkan untuk memastikan bahwa tata letak saat ini secara umum diselesaikan. Untuk melakukan ini, kita mendefinisikan metode isSolvable.

Jika tag tertentu didahului oleh tag dengan nilai yang lebih tinggi, ini dianggap sebagai inversi. Ketika tag kosong di tempat, jumlah inversi harus bahkan untuk memecahkan teka-teki. Jadi, kami menghitung jumlah inversi dan mengembalikan true jika jumlahnya genap.

Maka penting untuk mendefinisikan metode isSolved untuk memeriksa apakah penyelarasan Game Of Fifteen kami telah teratasi. Pertama, kita melihat di mana tag kosong itu berada. Jika di posisi awal, maka perataan saat ini adalah yang baru, yang belum diputuskan sebelumnya. Lalu kami beralih di atas ubin dalam urutan terbalik, dan jika nilai tag berbeda dari indeks +1 yang sesuai, kembalikan salah. Kalau tidak, sekarang saatnya untuk mengembalikan true di akhir metode, karena puzzle telah dipecahkan.

Metode lain untuk didefinisikan adalah newGame. Diperlukan untuk membuat instance baru dari game. Untuk melakukan ini, kami mengatur ulang lapangan bermain, lalu mengocoknya dan melanjutkan sampai posisi bermain dapat diselesaikan.

Berikut adalah contoh kode dengan logika tag kunci:
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; } 

Akhirnya, Anda perlu memprogram pergerakan tempat dalam array. Kode ini akan dipanggil nanti melalui panggilan balik untuk merespons pergerakan kursor. Game kami akan mendukung beberapa gerakan ubin secara bersamaan. Jadi, setelah kami mengubah posisi yang ditekan pada layar menjadi sebuah tag, kami mendapatkan posisi tag yang kosong dan mencari arah gerakan untuk mendukung beberapa gerakannya pada saat yang bersamaan.

Berikut ini contoh kode:
 // 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 mengembangkan UI di Swing API


Saatnya melakukan antarmuka. Pertama kita ambil kelas Jpanel. Lalu kami menggambar titik di lapangan - untuk menghitung ukuran masing-masing, kami akan menggunakan data yang ditentukan dalam parameter konstruktor permainan:
 gridSize = (dim -  2 * margin); tileSize = gridSize / size; 

Margin juga merupakan parameter yang ditentukan dalam konstruktor game.

Sekarang Anda perlu mendefinisikan metode drawGrid untuk menggambar kisi dan bintik-bintik di layar. Kami menganalisis larik tag dan mengonversi koordinat ke koordinat antarmuka pengguna. Kemudian gambar setiap tag dengan nomor yang sesuai 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); } } 

Akhirnya, kita mendefinisikan kembali metode paintComponent, yang merupakan turunan dari kelas JPane. Kemudian kami menggunakan metode drawGrid, dan setelah itu kami menggunakan metode drawStartMessage untuk menampilkan pesan yang meminta kami mengklik untuk memulai 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); } 

Kami merespons tindakan pengguna di UI


Agar game dapat berjalan, perlu untuk memproses tindakan pengguna di UI. Untuk melakukan ini, tambahkan implementasi MouseListener pada Jpanel dan kode untuk memindahkan tempat yang sudah 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 menempatkan kode di konstruktor dari kelas GameOfFeeneen. Pada akhirnya, kami memanggil metode newGame untuk memulai permainan baru.

Kode permainan lengkap


Langkah terakhir, sebelum Anda melihat permainan sedang beraksi, adalah mengumpulkan semua elemen kode bersama-sama. Inilah hasilnya:
 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, mainkan!


Saatnya memulai permainan dan memeriksanya. Kolom akan terlihat seperti ini:



Mencoba memecahkan teka-teki. Jika semuanya berjalan dengan baik, kami mendapatkan ini:



Itu saja. Apakah Anda berharap lebih? :)

Skillbox merekomendasikan:

Source: https://habr.com/ru/post/id445758/


All Articles