"پندرہ" یا "پندرہ" ایک سادہ لوجک گیم کی ایک بہترین مثال ہے جو پوری دنیا میں مشہور ہے۔ پہیلی کو حل کرنے کے لیے، آپ کو چھوٹے سے بڑے تک، نمبروں کے ساتھ مربعوں کو ترتیب دینے کی ضرورت ہے۔ یہ آسان نہیں ہے، لیکن یہ دلچسپ ہے۔
آج کے ٹیوٹوریل میں ہم آپ کو دکھائیں گے کہ کیسے جاوا 8 میں ایکلیپس کے ساتھ پندرہ کو تیار کیا جائے۔ UI تیار کرنے کے لیے ہم Swing API استعمال کریں گے۔
ہم آپ کو یاد دلاتے ہیں:"Habr" کے تمام قارئین کے لیے - "Habr" پروموشنل کوڈ کا استعمال کرتے ہوئے کسی بھی Skillbox کورس میں داخلہ لینے پر 10 rubles کی رعایت۔
اس مرحلے پر آپ کو خصوصیات کی وضاحت کرنے کی ضرورت ہے:
سائز - کھیل کے میدان کا سائز؛
nbTiles - فیلڈ میں ٹیگز کی تعداد۔ nbTiles = سائز*سائز - 1؛
ٹائلز ایک ٹیگ ہے جو عدد کی ایک جہتی صف ہے۔ ہر ٹیگ کو رینج [0, nbTiles] میں ایک منفرد قدر ملے گی۔ صفر ایک خالی مربع کی نشاندہی کرتا ہے۔
blankPos - خالی مربع کی پوزیشن۔
کھیل کی منطق
ہمیں ایک ری سیٹ طریقہ کی وضاحت کرنے کی ضرورت ہے جو ایک نئی گیم پوزیشن کو شروع کرنے کے لیے استعمال کیا جاتا ہے۔ اس طرح ہم ٹیگز کی صف کے ہر عنصر کے لیے ایک قدر مقرر کرتے ہیں۔ ٹھیک ہے، پھر ہم صف کی آخری پوزیشن میں خالی پوز رکھتے ہیں۔
ہمیں ٹیگز کی صف کو شفل کرنے کے لیے ایک شفل طریقہ کی بھی ضرورت ہے۔ ہم خالی ٹیگ کو شفل کرنے کے عمل میں شامل نہیں کرتے ہیں تاکہ اسے اسی پوزیشن میں رکھا جاسکے۔
چونکہ پہیلی کی ممکنہ ابتدائی پوزیشنوں میں سے صرف نصف کا حل ہے، اس لیے آپ کو اس بات کا یقین کرنے کے لیے کہ موجودہ ترتیب حل کرنے کے قابل بھی ہے اس کے نتیجے میں بدلنے والے نتائج کو چیک کرنے کی ضرورت ہے۔ ایسا کرنے کے لیے، ہم isSolvable طریقہ کی وضاحت کرتے ہیں۔
اگر کسی خاص ٹیگ سے پہلے کسی ٹیگ کی قیمت زیادہ ہوتی ہے تو اسے الٹا سمجھا جاتا ہے۔ جب خالی جگہ جگہ پر ہو، معما کو حل کرنے کے لیے معکوسوں کی تعداد یکساں ہونی چاہیے۔ لہٰذا ہم معکوسوں کی تعداد کو شمار کرتے ہیں اور اگر نمبر برابر ہو تو درست لوٹتے ہیں۔
اس کے بعد isSolved طریقہ کی وضاحت کرنا ضروری ہے کہ آیا ہمارا گیم آف ففٹین لے آؤٹ حل ہو گیا ہے۔ پہلے ہم دیکھتے ہیں کہ خالی جگہ کہاں ہے۔ اگر ابتدائی پوزیشن میں ہے، تو موجودہ سیدھ نئی ہے، پہلے سے طے شدہ نہیں ہے۔ اس کے بعد ہم ٹائلوں کے ذریعے ریورس ترتیب میں اعادہ کرتے ہیں، اور اگر ٹیگ کی قدر متعلقہ انڈیکس +1 سے مختلف ہے، تو ہم غلط واپس کرتے ہیں۔ بصورت دیگر، طریقہ کے اختتام پر یہ صحیح ہونے کا وقت ہے کیونکہ پہیلی پہلے ہی حل ہو چکی ہے۔
ایک اور طریقہ جس کی وضاحت کرنے کی ضرورت ہے وہ ہے نیو گیم۔ گیم کی ایک نئی مثال بنانے کی ضرورت ہے۔ ایسا کرنے کے لیے، ہم کھیل کے میدان کو دوبارہ ترتیب دیتے ہیں، پھر اسے شفل کرتے ہیں اور اس وقت تک جاری رکھیں جب تک کہ کھیل کی پوزیشن حل نہ ہو جائے۔
یہاں ٹیگ کی کلیدی منطق کے ساتھ ایک مثالی کوڈ ہے:
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;
ہم سوئنگ API کا استعمال کرتے ہوئے UI تیار کرتے ہیں۔
یہ انٹرفیس پر کام کرنے کا وقت ہے. پہلے ہم Jpanel کلاس لیتے ہیں۔ پھر ہم فیلڈ پر ٹیگ بناتے ہیں - ہر ایک کے سائز کا حساب لگانے کے لیے، ہم گیم کنسٹرکٹر پیرامیٹر میں بیان کردہ ڈیٹا استعمال کریں گے:
اب ہمیں اسکرین پر گرڈ اور دھبوں کو کھینچنے کے لیے ڈرا گرڈ کے طریقہ کار کی وضاحت کرنے کی ضرورت ہے۔ ہم ٹیگز کی صف کا تجزیہ کرتے ہیں اور کوآرڈینیٹس کو یوزر انٹرفیس کوآرڈینیٹ میں تبدیل کرتے ہیں۔ پھر مرکز میں متعلقہ نمبر کے ساتھ ہر جگہ کو کھینچیں:
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);
}
}
آخر میں، آئیے پینٹ کومپوننٹ طریقہ کو اوور رائیڈ کرتے ہیں، جو JPane کلاس سے ماخوذ ہے۔ اس کے بعد ہم ڈرا گرڈ کا طریقہ استعمال کرتے ہیں، اس کے بعد ڈرا اسٹارٹ میسج کا طریقہ استعمال کرتے ہیں تاکہ ہمیں گیم شروع کرنے کے لیے کلک کرنے کا اشارہ دیا جائے۔
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 میں صارف کی کارروائیوں پر ردعمل
گیم اپنے کورس کو چلانے کے لیے، UI میں صارف کے اعمال پر کارروائی کرنا ضروری ہے۔ ایسا کرنے کے لیے، Jpanel پر MouseListener کا نفاذ اور حرکت پذیر مقامات کے لیے کوڈ شامل کریں، جو پہلے ہی اوپر دکھایا گیا ہے:
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 طریقہ کہتے ہیں۔
مکمل گیم کوڈ
کھیل کو عملی شکل میں دیکھنے سے پہلے آخری مرحلہ تمام کوڈ عناصر کو ایک ساتھ رکھنا ہے۔ یہاں کیا ہوتا ہے:
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);
});
}
}
آخر میں، چلو کھیلیں!
یہ گیم لانچ کرنے اور اسے عملی شکل میں جانچنے کا وقت ہے۔ فیلڈ کو اس طرح نظر آنا چاہئے:
آئیے اس پہیلی کو حل کرنے کی کوشش کرتے ہیں۔ اگر سب کچھ ٹھیک رہا تو ہمیں یہ ملتا ہے: