
كيفية برمجة لعبة الألغاز المنزلقة Sliding Tile Puzzle على اندرويد ستوديو | جافا
في هذا المقال، سنتعلم كيفية برمجة لعبة الألغاز المنزلقة الكلاسيكية
لنظام Android باستخدام Android Studio ولغة Java.
ستقوم اللعبة بتقسيم صورة مختارة إلى مربعات صغيرة عشوائيًا، وسيكون على
اللاعب إعادة ترتيب هذه المربعات عن طريق تحريكها إلى المربع الفارغ حتى تتكون الصورة الأصلية.
خطوات برمجة لعبة الألغاز المنزلقة Sliding Tile Puzzle
المتطلبات:
Android Studio مثبت، معرفة جيدة بلغة Java وتطوير تطبيقات Android،
صورة للاستخدام في اللغز (يمكن أن تكون في مجلد res/drawable).
- MainActivity: النشاط الرئيسي الذي يعرض اللغز.
- PuzzleView: فئة مخصصة (توسيع View) لرسم الألغاز والتعامل مع لمسات المستخدم.
- PuzzlePiece: فئة لتمثيل كل قطعة من قطع اللغز (تحتوي على جزء الصورة وموقعها).
- GameLogic: فئة تحتوي على منطق اللعبة (إنشاء اللغز، خلطه، التحقق من الحركة، التحقق من الفوز).
الخطوة 1: إعداد المشروع والإذن (AndroidManifest.xml)
قم بإنشاء مشروع Android جديد في Android Studio.
تأكد من أن لديك إذنًا لقراءة التخزين الخارجي إذا كنت تخطط للسماح للمستخدمين
باختيار صورهم (اختياري، في هذا المثال سنستخدم صورة من الموارد).
XML
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.slidingpuzzle">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SlidingPuzzle"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>
--
الخطوة 2: فئة PuzzlePiece
هذه الفئة ستمثل كل مربع في اللغز.
Java
import android.graphics.Bitmap;
public class PuzzlePiece {
public int x; // الموضع الحالي x في الشبكة
public int y; // الموضع الحالي y في الشبكة
public int originalX; // الموضع الأصلي x في الشبكة (للتأكد من الحل)
public int originalY; // الموضع الأصلي y في الشبكة (للتأكد من الحل)
public Bitmap image; // الجزء من الصورة الذي يمثله المربع
public PuzzlePiece(int originalX, int originalY, Bitmap image) {
this.originalX = originalX;
this.originalY = originalY;
this.image = image;
// الموضع الحالي يتم تحديده لاحقًا عند خلط اللغز
}
// لتحديد الموضع الحالي بعد الخلط
public void setCurrentPosition(int x, int y) {
this.x = x;
this.y = y;
}
// هل هذا المربع في مكانه الصحيح؟
public boolean isInCorrectPlace() {
return x == originalX && y == originalY;
}
}
--
الخطوة 3: فئة GameLogic
هذه الفئة ستحتوي على المنطق الرئيسي للعبة.
Java
import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.content.Context;import android.graphics.Matrix; // تأكد من استيراد هذه
import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Random;
public class GameLogic { private Context context; private Bitmap originalBitmap; private int rows; private int cols; private List<PuzzlePiece> puzzlePieces; private int emptyPieceX; private int emptyPieceY; // موقع المربع الفارغ في الشبكة
// Constructor كما هو public GameLogic(Context context, int imageResId, int rows, int cols) { this.context = context; this.rows = rows; this.cols = cols; this.originalBitmap = BitmapFactory.decodeResource(context.getResources(), imageResId); this.puzzlePieces = new ArrayList<>(); createPuzzlePieces(); emptyPieceX = cols - 1; // المربع الفارغ يبدأ في الزاوية اليمنى السفلية emptyPieceY = rows - 1; }
// Constructor بديل إذا كنت تريد تمرير Bitmap مباشرة (مثل ميزة اختيار الصورة) public GameLogic(Context context, Bitmap bitmap, int rows, int cols) { this.context = context; this.originalBitmap = bitmap; this.rows = rows; this.cols = cols; this.puzzlePieces = new ArrayList<>(); createPuzzlePieces(); emptyPieceX = cols - 1; emptyPieceY = rows - 1; }
private void createPuzzlePieces() { int pieceWidth = originalBitmap.getWidth() / cols; int pieceHeight = originalBitmap.getHeight() / rows;
for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { if (i == rows - 1 && j == cols - 1) { // المربع الفارغ، لا ننشئ له صورة. يمثل بالـ index الأخير puzzlePieces.add(new PuzzlePiece(j, i, null)); } else { Bitmap pieceBitmap = Bitmap.createBitmap(originalBitmap, j * pieceWidth, i * pieceHeight, pieceWidth, pieceHeight); puzzlePieces.add(new PuzzlePiece(j, i, pieceBitmap)); } } } }
// *** بداية تعديلات خوارزمية الخلط وقابلية الحل ***
public void shufflePuzzle() { // قائمة تمثل الترتيب الحالي للقطع بناءً على originalX, originalY List<Integer> currentOrder = new ArrayList<>(); for (int i = 0; i < rows * cols; i++) { // إضافة index الأصلي لكل قطعة. القطعة الأخيرة (الفارغة) هي (rows*cols - 1) currentOrder.add(i); }
Random random = new Random(System.nanoTime()); // استخدام وقت النظام لبذور عشوائية
// خلط الترتيب حتى يصبح اللغز قابلاً للحل do { Collections.shuffle(currentOrder, random); // تحديد موقع المربع الفارغ في الترتيب المخلط int emptyPieceCurrentIndex = -1; for(int i=0; i<currentOrder.size(); i++){ if(currentOrder.get(i) == rows * cols - 1){ // إذا كانت القيمة هي index القطعة الفارغة emptyPieceCurrentIndex = i; break; } } emptyPieceY = emptyPieceCurrentIndex / cols; // الصف الحالي للقطعة الفارغة emptyPieceX = emptyPieceCurrentIndex % cols; // العمود الحالي للقطعة الفارغة
} while (!isSolvable(currentOrder, emptyPieceY));
// تطبيق الترتيب الجديد على قطع اللغز for (int i = 0; i < puzzlePieces.size(); i++) { int newPosIndex = currentOrder.get(i); // هذا هو الـ original index الذي يجب أن يكون في هذا المكان // البحث عن القطعة التي لها هذا الـ original index وتعيينها في هذا الموضع for(PuzzlePiece piece : puzzlePieces){ if(piece.originalX + piece.originalY * cols == i){ // إذا كانت هذه هي القطعة الأصلية في هذا الـ index piece.setCurrentPosition(newPosIndex % cols, newPosIndex / cols); break; } } }
// تحديث موقع المربع الفارغ في الفئة for (PuzzlePiece piece : puzzlePieces) { if (piece.image == null) { emptyPieceX = piece.x; emptyPieceY = piece.y; break; } } }
// حساب عدد الانقلابات private int getInversions(List<Integer> arr) { int inversions = 0; int n = arr.size(); for (int i = 0; i < n - 1; i++) { for (int j = i + 1; j < n; j++) { // تجاهل المربع الفارغ عند حساب الانقلابات if (arr.get(i) == rows * cols - 1 || arr.get(j) == rows * cols - 1) { continue; } if (arr.get(i) > arr.get(j)) { inversions++; } } } return inversions; }
// التحقق من قابلية الحل private boolean isSolvable(List<Integer> currentOrder, int emptyPieceRow) { int inversions = getInversions(currentOrder);
if (cols % 2 == 1) { // شبكة عرضها فردي (مثل 3x3, 5x5) // قابل للحل إذا كان عدد الانقلابات زوجيًا return (inversions % 2 == 0); } else { // شبكة عرضها زوجي (مثل 4x4) // نحتاج إلى رقم الصف للمربع الفارغ (من الأسفل) int rowFromBottom = rows - emptyPieceRow; // 1-based from bottom // قابل للحل إذا (عدد الانقلابات + رقم الصف من الأسفل) زوجيًا return ((inversions + rowFromBottom) % 2 == 0); } }
// *** نهاية تعديلات خوارزمية الخلط وقابلية الحل ***
public List<PuzzlePiece> getPuzzlePieces() { return puzzlePieces; }
public int getEmptyPieceX() { return emptyPieceX; }
public int getEmptyPieceY() { return emptyPieceY; }
// محاولة تحريك قطعة public boolean movePiece(int clickedX, int clickedY) { // التحقق مما إذا كانت القطعة التي تم النقر عليها مجاورة للمربع الفارغ if ((Math.abs(clickedX - emptyPieceX) == 1 && clickedY == emptyPieceY) || (Math.abs(clickedY - emptyPieceY) == 1 && clickedX == emptyPieceX)) {
// العثور على القطعة التي تم النقر عليها PuzzlePiece clickedPiece = null; for (PuzzlePiece piece : puzzlePieces) { if (piece.x == clickedX && piece.y == clickedY) { clickedPiece = piece; break; } }
if (clickedPiece != null) { // العثور على القطعة الفارغة في القائمة PuzzlePiece emptyPiece = null; for (PuzzlePiece piece : puzzlePieces) { if (piece.image == null) { emptyPiece = piece; break; } }
// تبديل مواضع القطعة التي تم النقر عليها مع القطعة الفارغة int tempX = clickedPiece.x; int tempY = clickedPiece.y;
clickedPiece.setCurrentPosition(emptyPiece.x, emptyPiece.y); emptyPiece.setCurrentPosition(tempX, tempY); // تحديث موضع القطعة الفارغة
emptyPieceX = emptyPiece.x; // تحديث موقع المربع الفارغ في الفئة emptyPieceY = emptyPiece.y; // تحديث موقع المربع الفارغ في الفئة
return true; // تم التحريك بنجاح } } return false; // لا يمكن التحريك }
// التحقق من فوز اللاعب public boolean isSolved() { for (PuzzlePiece piece : puzzlePieces) { if (!piece.isInCorrectPlace()) { return false; } } return true; }}
--
الخطوة 4: فئة PuzzleView
هذه الفئة هي حيث سيتم رسم اللغز والتعامل مع تفاعلات اللمس.
Java
import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.view.MotionEvent;import android.view.View;import android.widget.Toast;
public class PuzzleView extends View { private GameLogic gameLogic; private int puzzleWidth; private int puzzleHeight; private int pieceWidth; private int pieceHeight; private int rows; private int cols; private Paint borderPaint;
public PuzzleView(Context context, int imageResId, int rows, int cols) { super(context); this.rows = rows; this.cols = cols; this.gameLogic = new GameLogic(context, imageResId, rows, cols); this.gameLogic.shufflePuzzle(); // خلط اللغز عند الإنشاء
borderPaint = new Paint(); borderPaint.setColor(Color.GRAY); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(5); }
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); puzzleWidth = w; puzzleHeight = h; pieceWidth = puzzleWidth / cols; pieceHeight = puzzleHeight / rows; }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (PuzzlePiece piece : gameLogic.getPuzzlePieces()) { if (piece.image != null) { // لا نرسم المربع الفارغ canvas.drawBitmap(piece.image, piece.x * pieceWidth, piece.y * pieceHeight, null); } // رسم الحدود لكل مربع (اختياري) canvas.drawRect(piece.x * pieceWidth, piece.y * pieceHeight, (piece.x + 1) * pieceWidth, (piece.y + 1) * pieceHeight, borderPaint); } }
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { int clickedX = (int) (event.getX() / pieceWidth); int clickedY = (int) (event.getY() / pieceHeight);
if (gameLogic.movePiece(clickedX, clickedY)) { invalidate(); // إعادة رسم اللغز بعد الحركة if (gameLogic.isSolved()) { Toast.makeText(getContext(), "تهانينا! لقد حللت اللغز!", Toast.LENGTH_LONG).show(); // يمكنك هنا إضافة منطق للعبة جديدة أو شاشة فوز } } return true; } return super.onTouchEvent(event); }}
--
الخطوة 5: تعديل MainActivity
هنا سنقوم بتهيئة PuzzleView وعرضها.
Java
import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.widget.LinearLayout;import android.widget.Toast;import android.view.ViewTreeObserver;import android.graphics.BitmapFactory;import android.graphics.Bitmap;
public class MainActivity extends AppCompatActivity {
private PuzzleView puzzleView; private int rows = 3; // عدد الصفوف (يمكن تغييرها) private int cols = 3; // عدد الأعمدة (يمكن تغييرها) private int imageResId = R.drawable.my_puzzle_image; // استبدل بمسار الصورة
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // تأكد أن لديك activity_main.xml بسيط
LinearLayout layout = findViewById(R.id.puzzle_container); // تأكد أن لديك LinearLayout في activity_main.xml puzzleView = new PuzzleView(this, imageResId, rows, cols); layout.addView(puzzleView); }
// تأكد من أن لديك هذه التخطيط في res/layout/activity_main.xml /* <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/puzzle_container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
</LinearLayout> */}
--
الخطوة 6: إضافة الصورة إلى المشروع
ضع الصورة التي تريد استخدامها للغز في مجلد res/drawable.
تأكد من أن اسمها my_puzzle_image.png
( أو قم بتغيير imageResId في MainActivity).
السماح للمستخدم باختيار صور من معرض الصور الخاص به.
أولاً، تأكد من أذونات التخزين في AndroidManifest.xml (كما في الخطوة 1).
* في MainActivity أو StartActivity:
أضف زرًا "اختيار صورة".
عند النقر عليه، استخدم Intent لفتح معرض الصور.
Java
// في MainActivity أو StartActivity// ...import android.content.Intent;import android.net.Uri;import android.provider.MediaStore;import android.app.Activity; // لـ Activity.RESULT_OKimport android.graphics.Bitmap;import android.graphics.BitmapFactory;import java.io.InputStream;
private static final int PICK_IMAGE_REQUEST = 1; // كود طلب
// ... في onCreate أو بعد النقر على زر "اختيار صورة"chooseImageButton.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); startActivityForResult(intent, PICK_IMAGE_REQUEST);});
@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PICK_IMAGE_REQUEST && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { Uri imageUri = data.getData(); try { InputStream imageStream = getContentResolver().openInputStream(imageUri); Bitmap selectedImage = BitmapFactory.decodeStream(imageStream);
// هنا، بدلاً من imageResId، ستقوم بتمرير الـ Bitmap مباشرة إلى PuzzleView // ستحتاج إلى تعديل GameLogic و PuzzleView لقبول Bitmap مباشرة // بدلاً من imageResId.
// مثال على كيفية تمرير Bitmap (قد تحتاج لتغيير GameLogic constructor) // puzzleView = new PuzzleView(this, selectedImage, rows, cols); // layout.removeAllViews(); // إزالة الـ PuzzleView القديم إذا كان موجودًا // layout.addView(puzzleView);
} catch (Exception e) { e.printStackTrace(); Toast.makeText(this, "فشل تحميل الصورة", Toast.LENGTH_SHORT).show(); } }}
--
* تعديل GameLogic و PuzzleView لقبول Bitmap مباشرة:
Java
// في GameLogicprivate Bitmap originalBitmap;
public GameLogic(Context context, Bitmap bitmap, int rows, int cols) { // constructor جديد this.context = context; this.originalBitmap = bitmap; this.rows = rows; this.cols = cols; this.puzzlePieces = new ArrayList<>(); createPuzzlePieces(); emptyPieceX = cols - 1; emptyPieceY = rows - 1;}
// في PuzzleViewpublic PuzzleView(Context context, Bitmap bitmap, int rows, int cols) { // constructor جديد super(context); this.mainActivity = (MainActivity) context; this.rows = rows; this.cols = cols; this.gameLogic = new GameLogic(context, bitmap, rows, cols); // استخدام bitmap this.gameLogic.shufflePuzzle(); // ...}
--
الخطوة 7: اختبار وقابلية الحل (مهم!)
جزء خلط اللغز (shufflePuzzle) في GameLogic هو الأكثر تحديًا.
الخوارزمية البسيطة المستخدمة أعلاه (Collections.shuffle) قد لا تضمن
أن اللغز قابل للحل دائمًا، خاصة للشبكات الأكبر.
تحتاج إلى تطبيق خوارزمية أكثر تعقيدًا تعتمد على "عدد الانقلابات"
(Inversion Count) للمصفوفة الناتجة. إذا كان عدد الانقلابات (مع الأخذ في الاعتبار
موضع المربع الفارغ في الشبكات الزوجية) زوجيًا، فاللغز قابل للحل.
مميزات اضافية لبرمجة لعبة الألغاز المنزلقة Sliding Tile Puzzle
اليك تفاصيل برمجية للميزات الإضافية للعبة الألغاز المنزلقة وسنركز على الأجزاء الأكثر أهمية لكل ميزة.
1. الأصوات: إضافة مؤثرات صوتية للحركة، وعند حل اللغز.
لإضافة الأصوات، سنستخدم SoundPool للمؤثرات الصوتية القصيرة،
ونضع ملفات الصوت في مجلد res/raw.
* أولاً، أضف ملفات الصوت:
قم بإنشاء مجلد raw داخل مجلد res إذا لم يكن موجودًا (res/raw/).
ضع ملفات الصوت فيه (مثال: move_tile.wav, puzzle_solved.wav).
* في فئة PuzzleView:
Java
import android.media.AudioManager;import android.media.SoundPool;import android.os.Build;// ... باقي الـ imports
public class PuzzleView extends View { // ... المتغيرات الموجودة مسبقًا private SoundPool soundPool; private int moveSoundId; private int solvedSoundId;
public PuzzleView(Context context, int imageResId, int rows, int cols) { super(context); // ... تهيئة gameLogic و borderPaint
// تهيئة SoundPool if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { soundPool = new SoundPool.Builder() .setMaxStreams(3) // عدد الأصوات التي يمكن تشغيلها في نفس الوقت .build(); } else { soundPool = new SoundPool(3, AudioManager.STREAM_MUSIC, 0); }
// تحميل الأصوات moveSoundId = soundPool.load(context, R.raw.move_tile, 1); solvedSoundId = soundPool.load(context, R.raw.puzzle_solved, 1); }
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { int clickedX = (int) (event.getX() / pieceWidth); int clickedY = (int) (event.getY() / pieceHeight);
if (gameLogic.movePiece(clickedX, clickedY)) { invalidate(); // إعادة رسم اللغز بعد الحركة soundPool.play(moveSoundId, 1, 1, 0, 0, 1); // تشغيل صوت الحركة
if (gameLogic.isSolved()) { Toast.makeText(getContext(), "تهانينا! لقد حللت اللغز!", Toast.LENGTH_LONG).show(); soundPool.play(solvedSoundId, 1, 1, 0, 0, 1); // تشغيل صوت الفوز // يمكنك هنا إضافة منطق للعبة جديدة أو شاشة فوز } } return true; } return super.onTouchEvent(event); }
// تحرير موارد SoundPool عند تدمير الـ View @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (soundPool != null) { soundPool.release(); soundPool = null; } }}
--
2. المؤقت: عرض الوقت المستغرق لحل اللغز.
* في فئة MainActivity:
سنستخدم Chronometer أو Handler مع Runnable لتحديث الوقت.
Chronometer أسهل للاستخدام.
Java
import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.os.SystemClock; // لإدارة وقت Chronometerimport android.widget.LinearLayout;import android.widget.Chronometer; // استيراد Chronometer
public class MainActivity extends AppCompatActivity {
private PuzzleView puzzleView; private int rows = 3; private int cols = 3; private int imageResId = R.drawable.my_puzzle_image; private Chronometer chronometer; // إضافة Chronometer
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
LinearLayout layout = findViewById(R.id.puzzle_container); chronometer = findViewById(R.id.chronometer); // التأكد من وجوده في activity_main.xml
puzzleView = new PuzzleView(this, imageResId, rows, cols); layout.addView(puzzleView);
// بدء المؤقت عند بدء اللعبة chronometer.setBase(SystemClock.elapsedRealtime()); chronometer.start(); }
// يمكنك إضافة طريقة لإيقاف المؤقت عند حل اللغز public void stopTimer() { if (chronometer != null) { chronometer.stop(); } }
// يمكنك إعادة ضبط المؤقت إذا بدأت لعبة جديدة public void resetTimer() { if (chronometer != null) { chronometer.setBase(SystemClock.elapsedRealtime()); } }}
--
* تعديل activity_main.xml لإضافة Chronometer:
XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/puzzle_container_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Chronometer
android:id="@+id/chronometer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:padding="8dp" />
<LinearLayout
android:id="@+id/puzzle_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
--
* تعديل PuzzleView لإعلام MainActivity بحل اللغز:
Java
public class PuzzleView extends View {
// ... المتغيرات الموجودة مسبقًا
private MainActivity mainActivity; // مرجع للـ Activity
public PuzzleView(Context context, int imageResId, int rows, int cols) {
super(context);
this.mainActivity = (MainActivity) context; // للحصول على مرجع للـ Activity
// ...
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// ...
if (gameLogic.movePiece(clickedX, clickedY)) {
invalidate();
// ...
if (gameLogic.isSolved()) {
Toast.makeText(getContext(), "تهانينا! لقد حللت اللغز!", Toast.LENGTH_LONG).show();
mainActivity.stopTimer(); // إيقاف المؤقت
// ...
}
}
return true;
}
}
--
3. عدد الحركات: عرض عدد الحركات التي قام بها اللاعب.
* في فئة MainActivity:
Java
import android.widget.TextView; // استيراد TextView
public class MainActivity extends AppCompatActivity {
// ... المتغيرات الموجودة مسبقًا
private TextView movesCountTextView;
private int movesCount = 0; // عداد للحركات
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
movesCountTextView = findViewById(R.id.movesCountTextView); // التأكد من وجوده في activity_main.xml
updateMovesCountDisplay(); // تحديث العرض الأولي
}
// طريقة لزيادة عدد الحركات وتحديث العرض
public void incrementMovesCount() {
movesCount++;
updateMovesCountDisplay();
}
private void updateMovesCountDisplay() {
movesCountTextView.setText("الحركات: " + movesCount);
}
// طريقة لإعادة ضبط عدد الحركات (لبدء لعبة جديدة)
public void resetMovesCount() {
movesCount = 0;
updateMovesCountDisplay();
}
}
--
* تعديل activity_main.xml لإضافة TextView للحركات:
XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout ...>
<Chronometer ... />
<TextView
android:id="@+id/movesCountTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="الحركات: 0"
android:padding="8dp"/>
<LinearLayout ...>
</LinearLayout>
</LinearLayout>
--
* تعديل PuzzleView لزيادة عدد الحركات:
Java
public class PuzzleView extends View { // ... المتغيرات الموجودة مسبقًا private MainActivity mainActivity; // مرجع للـ Activity
public PuzzleView(Context context, int imageResId, int rows, int cols) { super(context); this.mainActivity = (MainActivity) context; // للحصول على مرجع للـ Activity // ... }
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { int clickedX = (int) (event.getX() / pieceWidth); int clickedY = (int) (event.getY() / pieceHeight);
if (gameLogic.movePiece(clickedX, clickedY)) { invalidate(); // إعادة رسم اللغز بعد الحركة soundPool.play(moveSoundId, 1, 1, 0, 0, 1); mainActivity.incrementMovesCount(); // زيادة عدد الحركات
if (gameLogic.isSolved()) { // ... } } return true; } return super.onTouchEvent(event); }}
--
4. مستويات الصعوبة: السماح للمستخدم باختيار حجم الشبكة (3x3, 4x4, 5x5).
بدلاً من تعيين rows و cols مباشرة في MainActivity، يمكنك
السماح للمستخدم باختيارها من قائمة أو أزرار في شاشة البداية، ثم
تمريرها إلى MainActivity عبر Intent.
* مثال (شاشة البداية):
- إنشاء StartActivity (نشاط جديد) مع تخطيط (activity_start.xml)
يحتوي على أزرار لكل مستوى صعوبة (مثل "سهل 3x3", "متوسط 4x4", "صعب 5x5").
- عند النقر على الزر، قم بإنشاء Intent لتشغيل MainActivity و
مرر عدد الصفوف والأعمدة باستخدام putExtra().
Java
// StartActivity.javaimport androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.os.Bundle;import android.widget.Button;
public class StartActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_start);
Button easyButton = findViewById(R.id.easyButton); Button mediumButton = findViewById(R.id.mediumButton); Button hardButton = findViewById(R.id.hardButton);
easyButton.setOnClickListener(v -> startGame(3, 3)); mediumButton.setOnClickListener(v -> startGame(4, 4)); hardButton.setOnClickListener(v -> startGame(5, 5)); }
private void startGame(int rows, int cols) { Intent intent = new Intent(this, MainActivity.class); intent.putExtra("rows", rows); intent.putExtra("cols", cols); startActivity(intent); finish(); // إنهاء نشاط البدء }}
--
* XML :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout ...>
<TextView android:text="اختر مستوى الصعوبة" .../>
<Button android:id="@+id/easyButton" android:text="سهل (3x3)" .../>
<Button android:id="@+id/mediumButton" android:text="متوسط (4x4)" .../>
<Button android:id="@+id/hardButton" android:text="صعب (5x5)" .../>
</LinearLayout>
--
* تعديل MainActivity لاستقبال القيم من Intent:
Java
// MainActivity.java
// ...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// استقبال القيم من Intent
rows = getIntent().getIntExtra("rows", 3); // القيمة الافتراضية 3x3
cols = getIntent().getIntExtra("cols", 3);
// ... تهيئة المكونات الأخرى
puzzleView = new PuzzleView(this, imageResId, rows, cols);
// ...
}
--
* تعديل AndroidManifest.xml لتعيين StartActivity كنقطة دخول:
XML
<application ...>
<activity
android:name=".StartActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false"> </activity>
</application>
--
5. شاشة بداية/نهاية:
شاشة البداية: تم تناولها جزئيًا في ميزة "مستويات الصعوبة" (باستخدام StartActivity).
* شاشة النهاية (عند الفوز):
عندما gameLogic.isSolved() تصبح true في PuzzleView، يمكنك
إطلاق Intent إلى GameOverActivity (تمامًا كما فعلنا في لعبة Endless Runner).
يمكنك تمرير الوقت وعدد الحركات إلى شاشة النهاية.
6. حفظ التقدم: حفظ حالة اللغز للاعب للعودة إليه لاحقًا.
- ستحتاج إلى حفظ مصفوفة ترتيب القطع وموقع القطعة الفارغة في التخزين الدائم
(مثل SharedPreferences أو قاعدة بيانات SQLite أو ملف).
- عند الخروج من التطبيق (onPause() أو onStop() في MainActivity):
قم بتحويل List<PuzzlePiece> إلى تنسيق يمكن حفظه (مثل String بفاصلات، أو JSON).
- حفظ هذه البيانات في SharedPreferences.
- عند بدء التطبيق (onCreate() في MainActivity):
تحقق مما إذا كانت هناك بيانات محفوظة.
إذا وجدت، قم بتحميلها وإعادة بناء حالة اللغز بدلاً من إنشاء لغز جديد عشوائي.
Java
// مثال بسيط جداً لحفظ حالة اللغز في SharedPreferences (في MainActivity)import android.content.SharedPreferences;import com.google.gson.Gson; // ستحتاج لإضافة مكتبة Gson في build.gradle (implementation 'com.google.code.gson:gson:2.8.9')import com.google.gson.reflect.TypeToken;import java.lang.reflect.Type;
public class MainActivity extends AppCompatActivity { // ... private static final String PREFS_NAME = "PuzzlePrefs"; private static final String PUZZLE_STATE_KEY = "PuzzleState"; private SharedPreferences sharedPreferences;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
// ... تهيئة Chronometer, TextView
// محاولة تحميل حالة اللغز المحفوظة String savedPuzzleStateJson = sharedPreferences.getString(PUZZLE_STATE_KEY, null); if (savedPuzzleStateJson != null) { Gson gson = new Gson(); Type type = new TypeToken<List<PuzzlePiece>>(){}.getType(); List<PuzzlePiece> savedPieces = gson.fromJson(savedPuzzleStateJson, type);
// ستحتاج إلى تعديل GameLogic لإعادة تحميل هذه القطع // gameLogic = new GameLogic(this, imageResId, rows, cols, savedPieces); // ثم قم بتحديث emptyPieceX و emptyPieceY في GameLogic } else { // إنشاء لعبة جديدة puzzleView = new PuzzleView(this, imageResId, rows, cols); } // ... إضافة puzzleView إلى layout }
@Override protected void onPause() { // أو onStop() super.onPause(); // حفظ حالة اللغز if (puzzleView != null && puzzleView.getGameLogic() != null) { Gson gson = new Gson(); String json = gson.toJson(puzzleView.getGameLogic().getPuzzlePieces()); sharedPreferences.edit().putString(PUZZLE_STATE_KEY, json).apply(); // قد تحتاج أيضًا لحفظ movesCount و chronometer base time } } // ...}
--
تذكر أن كل ميزة قد تتطلب بعض التعديلات والتنسيق بين الفئات المختلفة.
ابدأ بتنفيذ ميزة واحدة في كل مرة واختبرها جيدًا.
* الخلاصة :
تعد برمجة لعبة الألغاز المنزلقة مشروعًا ممتازًا لتعلم كيفية التعامل مع
الرسومات (Bitmaps و Canvas)، وإدارة أحداث اللمس، وتنفيذ
منطق اللعبة المعقد في Android Studio باستخدام Java.
الأكواد المقدمة توفر أساسًا قويًا يمكنك البناء عليه لإضافة المزيد من الميزات
وتحسين تجربة المستخدم. تذكر أهمية خوارزمية الخلط لضمان قابلية حل اللغز.
حظًا موفقًا في مشروعك!