القائمة الرئيسية

الصفحات

كيفية برمجة لعبة الألغاز المنزلقة Sliding Tile Puzzle على اندرويد ستوديو | جافا

Android Studio، Java، How to Create a sliding tile puzzle game in Android Studio | Java، How-to-Create-sliding-tile-puzzle-game-in-Android-Studio-Java، How to program a sliding tile puzzle game in Android Studio | Java، لعبة ألغاز منزلقة أندرويد، Sliding Puzzle Android Java، تطوير ألعاب أندرويد جافا، معالجة الصور أندرويد، Bitmaps أندرويد، Canvas أندرويد، Android Studio لعبة ألغاز، أحداث اللمس أندرويد، خوارزمية الألغاز المنزلقة، لعبة الألغاز المنزلقة Sliding Tile Puzzle، برمجة لعبة الألغاز المنزلقة Sliding Tile Puzzle على اندرويد ستوديو بلغة جافا، برمجة لعبة الألغاز المنزلقة (Sliding Tile Puzzle) لنظام Android باستخدام Java، كيفية برمجة لعبة الألغاز المنزلقة Sliding Tile Puzzle على اندرويد ستوديو جافا، Sliding Puzzle Android Java، تطوير ألعاب أندرويد جافا،




كيفية برمجة لعبة الألغاز المنزلقة 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_OK
import 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);
});

@Override
protected 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




// في GameLogic
private 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;
}

// في PuzzleView
public 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; // لإدارة وقت Chronometer
import 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.java
import 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.
 الأكواد المقدمة توفر أساسًا قويًا يمكنك البناء عليه لإضافة المزيد من الميزات
 وتحسين تجربة المستخدم. تذكر أهمية خوارزمية الخلط لضمان قابلية حل اللغز. 
حظًا موفقًا في مشروعك!


جدول المحتويات