"خمسة عشر" أو "خمسة عشر" إنها مثال ممتاز للعبة المنطق البسيطة التي تحظى بشعبية كبيرة في جميع أنحاء العالم. لحل اللغز، عليك ترتيب المربعات بالأرقام بالترتيب، من الأصغر إلى الأكبر. انها ليست سهلة، ولكنها مثيرة للاهتمام.
سنوضح لك في البرنامج التعليمي اليوم كيفية تطوير خمسة عشر في Java 8 باستخدام Eclipse. لتطوير واجهة المستخدم، سنستخدم Swing API.
نذكر:لجميع قراء "Habr" - خصم 10 روبل عند التسجيل في أي دورة Skillbox باستخدام رمز "Habr" الترويجي.
يوصي Skillbox بما يلي: دورة تعليمية عبر الإنترنت "مهنة مطور جافا".
تصميم اللعبة
في هذه المرحلة تحتاج إلى تحديد الخصائص:
الحجم — حجم الملعب؛
nbTiles — عدد العلامات الموجودة في الحقل. nbTiles = size*size - 1;
Tiles عبارة عن علامة عبارة عن مجموعة أحادية البعد من الأعداد الصحيحة. ستتلقى كل علامة قيمة فريدة في النطاق [0، nbTiles]. يشير الصفر إلى مربع فارغ؛
BlankPos - موضع المربع الفارغ.
منطق اللعبة
نحتاج إلى تحديد طريقة إعادة التعيين المستخدمة لتهيئة موضع جديد للعبة. بهذه الطريقة نقوم بتعيين قيمة لكل عنصر في مصفوفة العلامات. حسنًا، بعد ذلك نضع نقطة فارغة في الموضع الأخير من المصفوفة.
نحتاج أيضًا إلى طريقة خلط عشوائي لخلط مجموعة العلامات. نحن لا نقوم بتضمين العلامة الفارغة في عملية الخلط من أجل تركها في نفس الموضع.
نظرًا لأن نصف مواضع البداية المحتملة للغز فقط لديها حل، فأنت بحاجة إلى التحقق من نتيجة الخلط الناتجة للتأكد من أن التخطيط الحالي قابل للحل. للقيام بذلك، نحدد طريقة isSolvable.
إذا كانت علامة معينة مسبوقة بعلامة ذات قيمة أعلى، فسيتم اعتبار ذلك انعكاسًا. عندما تكون البقعة الفارغة في مكانها، يجب أن يكون عدد الانقلابات متساويًا حتى يكون اللغز قابلاً للحل. لذا، فإننا نحسب عدد الانقلابات ونعود صحيحًا إذا كان الرقم زوجيًا.
من المهم بعد ذلك تحديد طريقة isSolved للتحقق مما إذا كان تخطيط Game Of Fifteen الخاص بنا قد تم حله. أولاً ننظر إلى مكان وجود البقعة الفارغة. إذا كان في الموضع الأولي، فإن المحاذاة الحالية جديدة، ولم يتم تحديدها مسبقًا. نقوم بعد ذلك بالتكرار عبر المربعات بترتيب عكسي، وإذا كانت قيمة العلامة مختلفة عن الفهرس المقابل +1، فإننا نعيد خطأ. بخلاف ذلك، في نهاية الطريقة، حان الوقت للعودة إلى الحقيقة لأن اللغز قد تم حله بالفعل.
الطريقة الأخرى التي تحتاج إلى تعريف هي 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;
نقوم بتطوير واجهة المستخدم باستخدام Swing API
حان الوقت للعمل على الواجهة. أولا نأخذ فئة Jpanel. ثم نرسم العلامات في الحقل - لحساب أحجام كل منها، سنستخدم البيانات المحددة في معلمة مُنشئ اللعبة:
نحن الآن بحاجة إلى تحديد طريقة 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);
}
الرد على إجراءات المستخدم في واجهة المستخدم
لكي تسير اللعبة في مسارها الصحيح، من الضروري معالجة إجراءات المستخدم في واجهة المستخدم. للقيام بذلك، أضف تطبيق 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);
});
}
}
وأخيرا، دعونا نلعب!
حان الوقت لإطلاق اللعبة واختبارها أثناء العمل. يجب أن يبدو الحقل هكذا:
دعونا نحاول حل اللغز. إذا سارت الأمور على ما يرام، نحصل على هذا: