"پانزده" یا "پانزده" یک مثال عالی از یک بازی منطقی ساده است که در سراسر جهان محبوب است. برای حل معما، باید مربع ها را با اعداد به ترتیب، از کوچک ترین به بزرگ، مرتب کنید. آسان نیست، اما جالب است.
در آموزش امروز به شما نشان می دهیم که چگونه Fifteen را در جاوا 8 با Eclipse توسعه دهید. برای توسعه UI، از Swing API استفاده خواهیم کرد.
یادآوری می کنیم:برای همه خوانندگان "Habr" - تخفیف 10 روبل هنگام ثبت نام در هر دوره Skillbox با استفاده از کد تبلیغاتی "Habr".
nbTiles - تعداد تگ های موجود در فیلد. nbTiles = اندازه * اندازه - 1;
Tiles یک تگ است که یک آرایه تک بعدی از اعداد صحیح است. هر یک از تگ ها یک مقدار منحصر به فرد در محدوده [0, nbTiles] دریافت خواهند کرد. صفر نشان دهنده مربع خالی است.
blankPos - موقعیت مربع خالی.
منطق بازی
ما باید یک روش بازنشانی تعریف کنیم که برای مقداردهی اولیه موقعیت بازی جدید استفاده می شود. به این صورت است که برای هر عنصر آرایه تگ یک مقدار تعیین می کنیم. خوب، سپس blankPos را در آخرین موقعیت آرایه قرار می دهیم.
همچنین برای به هم زدن آرایه تگ ها به یک متد shuffle نیاز داریم. ما یک تگ خالی را در فرآیند به هم زدن اضافه نمی کنیم تا آن را در موقعیت اصلی خود قرار دهیم.
از آنجایی که تنها نیمی از موقعیتهای شروع ممکن پازل راهحل دارند، نتیجه این همرسانی باید بررسی شود تا مطمئن شوید که طرح فعلی به طور کلی قابل حل است. برای این کار متد isSolvable را تعریف می کنیم.
اگر قبل از یک نقطه خاص، یک نقطه با مقدار بالاتر وجود داشته باشد، این یک وارونگی در نظر گرفته می شود. وقتی تگ خالی در جای خود قرار دارد، تعداد وارونگی ها باید زوج باشد تا معما قابل حل باشد. بنابراین تعداد وارونگیها را میشماریم و اگر عدد زوج باشد، true را برمیگردانیم.
در مرحله بعد، تعریف یک متد isSolved برای بررسی اینکه آیا دست Game Of Fifteen ما حل شده است، مهم است. ابتدا محل قرارگیری تگ خالی را بررسی می کنیم. اگر در موقعیت اولیه باشد، طرح فعلی جدید است، قبلا حل نشده است. سپس به ترتیب معکوس از میان کاشی ها حلقه می زنیم و اگر مقدار تگ با شاخص 1+ مربوطه متفاوت باشد، false را برمی گردانیم. در غیر این صورت، وقت آن است که در پایان روش به true برگردید زیرا معما قبلاً حل شده است.
روش دیگری که باید تعریف شود 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
برای اینکه بازی روند خود را اجرا کند، لازم است اقدامات کاربر در رابط کاربری انجام شود. برای انجام این کار، پیاده سازی 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();
}
});
کد در سازنده کلاس GameOffFifteen قرار می گیرد. در پایان، ما متد 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);
});
}
}
بالاخره بیایید بازی کنیم!
زمان راه اندازی بازی و آزمایش آن در عمل فرا رسیده است. فیلد باید به شکل زیر باشد:
بیایید سعی کنیم معما را حل کنیم. اگر همه چیز خوب پیش رفت، این را دریافت می کنیم: