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

الصفحات

كيفية برمجة لعبة سوليتير احترافية على NetBeans باستخدام Java

How to Code Solitaire Game on NetBeans in Java، Java Solitaire، NetBeans game، OOP Java، Card game Java،Swing GUI، Game logic Java، Mouse events Java، Drag and drop Java، Solitaire rules، Deck shuffling، Card images Game panel، JFrame، JPanel، Klondike Solitaire، برمجة لعبة Solitaire في NetBeans باستخدام Java، إنشاء لعبة بطاقات Solitaire باستخدام البرمجة الكائنية في Java، تطوير واجهة مستخدم رسومية (GUI) للعبة Solitaire في Java Swing، كيفية التعامل مع أحداث الماوس (السحب والإفلات والنقر) في لعبة Solitaire Java، تطبيق قواعد لعبة Klondike Solitaire في Java، تحميل وعرض صور البطاقات في تطبيق Java Swing، إعداد مشروع Java NetBeans للعبة بطاقات، بناء فئة Card في Java للعبة Solitaire، تصميم فئة Deck لإدارة البطاقات في Solitaire، تنفيذ منطق اللعبة (Game Logic) للعبة Solitaire في Java، تحسينات وميزات إضافية للعبة Solitaire في Java Swing، إضافة نظام تتبع النقاط والتراجع في لعبة Solitaire Java، NetBeans، واجهة المستخدم الرسومية (GUI)، وإدارة تفاعلات المستخدم،Image loading, resource path, card assets, image management, static initializer , IOException، JPanel, custom painting, Graphics2D, card drawing, layout management, responsive design, game state rendering، JFrame, main window, menu bar, event handling, mouse listeners, game initialization, SwingUtilities.invokeLater، Java game development, NetBeans project, Solitaire game, next steps, future enhancements, community involvement، Debugging, testing, game execution, NetBeans run, troubleshooting، Game features, score, undo, new game, winning condition, restart game, JMenuBar, JOptionPane، Game rules, validation logic, move validation, Klondike Solitaire rules, card movement, source pile, target pile، MouseListener, mouse events, drag and drop, game input, click events, drag events, drop events، GUI structure, JFrame, JPanel, Swing components, visual representation, card rendering, game layout, NetBeans GUI builder، Card class, Java enums, suit, rank, card state, faceUp, object-oriented programming، Deck class, card collection, shuffle cards, deal card, initialization, randomization، Game logic, Solitaire state, stock pile, waste pile, foundation piles, tableau piles, game initialization, card dealing, win condition, move validation، Game logic, card representation, deck management, game state, data structures, Enums, Card class, Suit enum, Rank enum, Deck class، NetBeans project, Java Application, project setup, main class, project structure، Java game development, NetBeans IDE, Solitaire game, object-oriented programming (OOP), GUI programming, card game, logic design, user interface، Java game development, NetBeans IDE, Solitaire game, object-oriented programming (OOP), GUI programming, card game، Game logic, card representation, deck management, game state, data structures, Enums، Java Swing, AWT, JPanel, JFrame, custom painting, rendering cards، MouseListener, mouse events, drag and drop, game input، Game rules, validation logic, move validation، Game features, score, undo, new game, winning condition، Debugging, testing, game execution, Java game development, NetBeans project, Solitaire game, next steps, تطوير ألعاب جافا، مشروع NetBeans، لعبة سوليتير، الخطوات التالية, تصحيح الأخطاء، الاختبار، تنفيذ اللعبة, ميزات اللعبة، النتيجة، التراجع، لعبة جديدة، شرط الفوز, قواعد اللعبة، منطق التحقق، التحقق من صحة الحركات, مستمع الماوس، أحداث الماوس، السحب والإفلات، مدخلات اللعبة, Java Swing، AWT، JPanel، JFrame، الرسم المخصص، عرض البطاقات, منطق اللعبة، تمثيل البطاقات، إدارة مجموعة البطاقات، حالة اللعبة، هياكل البيانات، التعدادات، مشروع NetBeans، تطبيق Java، إعداد المشروع، الفئة الرئيسية،تطوير ألعاب جافا، بيئة التطوير المتكاملة NetBeans، لعبة سوليتير، البرمجة الكائنية التوجه (OOP)، برمجة واجهة المستخدم الرسومية (GUI)، لعبة ورق،



كيفية برمجة لعبة سوليتير احترافية على NetBeans باستخدام Java


في عالم تطوير الألعاب باستخدام Java، تُعد لغة Java خيارًا ممتازًا لإنشاء ألعاب بسيطة لكنها جذابة.
 إذا كنت تبحث عن مشروع يجمع بين برمجة الألعاب وتطوير الواجهات الرسومية
 (GUI)، فإن لعبة Solitaire (الصبر) هي نقطة انطلاق مثالية.
 في هذا المقال المفصل، سنرشدك خطوة بخطوة عبر برمجة لعبة Solitaire
 احترافية من البداية باستخدام NetBeans IDE ومفاهيم البرمجة الكائنية (OOP).
 سنتناول كل جانب، من تصميم منطق اللعبة إلى بناء الواجهة الرسومية التفاعلية 
وتطبيق قواعد اللعبة.

1. مقدمة في تطوير لعبة سوليتير باستخدام Java & NetBeans


تُعد لعبة Solitaire الكلاسيكية تحديًا ممتعًا لبرمجة ألعاب الورق. ستمنحك
 هذه العملية فهمًا عميقًا لكيفية تمثيل الكائنات (Objects) في البرمجة، وإدارة الحالات
 (States) المعقدة، والتفاعل مع المستخدم (User Interaction) عبر
 واجهة رسومية (GUI). NetBeans IDE يوفر بيئة قوية وسهلة الاستخدام لـبرمجة Java، 
مما يجعله الخيار الأمثل لمشروعنا هذا. سنركز على إنشاء لعبة
 Solitaire (Klondike Solitaire) التي تتضمن الميزات الأساسية مثل سحب 
ورمي البطاقات، وتطبيق القواعد، والتحقق من الفوز.

2. إعداد مشروعك في NetBeans

سنبدأ بإنشاء مشروع Java جديد في NetBeans.

* الخطوات :
- افتح NetBeans IDE.
- انتقل إلى File > New Project... (أو Ctrl + Shift + N).
- في نافذة "New Project"، اختر Categories: Java with Ant
 (أو Java with Maven إذا كنت تفضل Maven)، ثم 
Projects: Java Application. انقر Next.
- في "New Java Application":
Project Name: SolitaireGame
- Project Location: اختر مجلدًا لحفظ مشروعك.
- Create Main Class: تأكد من تحديد هذا الخيار، واسم الفئة الرئيسية سيكون 
solitairegame.SolitaireGame (سيتم إنشاء حزمة solitairegame تلقائيًا).
انقر Finish.
سيقوم NetBeans بإنشاء هيكل المشروع الأساسي، بما في ذلك فئة SolitaireGame.java الرئيسية.

3. تصميم منطق اللعبة (الفئات الأساسية)

لبناء لعبة Solitaire، نحتاج إلى تمثيل البطاقات، مجموعات البطاقات (الرزم)،
 وحالة اللعبة بأكملها. سنستخدم نهج البرمجة الكائنية (OOP)
 لإنشاء فئات (Classes) تمثل هذه العناصر.

3.1. فئة Card (البطاقة)

تمثل هذه الفئة بطاقة لعب واحدة بخصائصها (الرتبة والنوع) وحالتها (مقلوبة أم مكشوفة).
إنشاء الفئة: في NetBeans، انقر بزر الماوس الأيمن على حزمة solitairegame في
 نافذة Projects، اختر New > Java Class...، واكتب Card كـ Class Name.

* لـ Card.java:
Java




package solitairegame;

public class Card {
    // تعاريف للمقاييس (Suits) والرتب (Ranks) باستخدام Enums
    public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
    public enum Rank { ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING }

    private final Suit suit; // نوع البطاقة (قلب، ديناري، إلخ)
    private final Rank rank; // رتبة البطاقة (الآس، 2، الملك، إلخ)
    private boolean isFaceUp; // حالة البطاقة: هل هي مكشوفة أم مقلوبة؟

    // منشئ (Constructor) للفئة Card
    public Card(Suit suit, Rank rank) {
        this.suit = suit;
        this.rank = rank;
        this.isFaceUp = false; // افتراضياً، تكون البطاقات مقلوبة عند الإنشاء
    }

    // دوال getters للوصول إلى خصائص البطاقة
    public Suit getSuit() {
        return suit;
    }

    public Rank getRank() {
        return rank;
    }

    public boolean isFaceUp() {
        return isFaceUp;
    }

    // دالة لتعيين حالة البطاقة (مكشوفة أو مقلوبة)
    public void setFaceUp(boolean faceUp) {
        isFaceUp = faceUp;
    }

    // دالة مساعدة للتحقق مما إذا كانت البطاقة حمراء اللون (قلب أو ديناري)
    public boolean isRed() {
        return this.suit == Suit.DIAMONDS || this.suit == Suit.HEARTS;
    }

    // دالة مساعدة للتحقق مما إذا كانت البطاقة سوداء اللون (سباتي أو سنبل)
    public boolean isBlack() {
        return this.suit == Suit.CLUBS || this.suit == Suit.SPADES;
    }

    // دالة toString لتحويل كائن البطاقة إلى تمثيل نصي (لأغراض التصحيح أو العرض)
    @Override
    public String toString() {
        return rank + " of " + suit;
    }
}



--

3.2. فئة Deck (رزمة البطاقات)

تمثل هذه الفئة رزمة بطاقات كاملة (52 بطاقة) وتوفر وظائف لإنشاء الرزمة، 
خلطها، وسحب البطاقات منها.
- إنشاء الفئة: أنشئ فئة جديدة باسم Deck.java بنفس الطريقة.
* لـ Deck.java:
Java




package solitairegame;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Deck {
    private List<Card> cards; // قائمة لتخزين البطاقات في الرزمة

    // منشئ (Constructor) للفئة Deck
    public Deck() {
        cards = new ArrayList<>();
        // إنشاء جميع البطاقات (52 بطاقة) وإضافتها إلى الرزمة
        for (Card.Suit suit : Card.Suit.values()) {
            for (Card.Rank rank : Card.Rank.values()) {
                cards.add(new Card(suit, rank));
            }
        }
    }

    // دالة لخلط البطاقات في الرزمة
    public void shuffle() {
        Collections.shuffle(cards); // استخدام دالة جاهزة لخلط القائمة
    }

    // دالة لسحب بطاقة واحدة من أعلى الرزمة
    public Card dealCard() {
        if (cards.isEmpty()) {
            return null; // إذا كانت الرزمة فارغة، لا توجد بطاقات لسحبها
        }
        return cards.remove(0); // سحب أول بطاقة من القائمة (أعلى الرزمة)
    }

    // دالة للتحقق مما إذا كانت الرزمة فارغة
    public boolean isEmpty() {
        return cards.isEmpty();
    }

    // دالة للحصول على عدد البطاقات المتبقية في الرزمة
    public int size() {
        return cards.size();
    }
}


--

3.3. فئة SolitaireGameLogic (منطق اللعبة)

هذه الفئة هي قلب اللعبة، حيث تحتوي على جميع القواعد والمنطق الخاص بلعبة السوليتير
 (Klondike). سنقوم بتعديلها لتكون منفصلة عن الواجهة الرسومية الرئيسية.
- إنشاء الفئة: أنشئ فئة جديدة باسم SolitaireGameLogic.java.

*  لـ SolitaireGameLogic.java:
Java




package solitairegame;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack; // Stack هو نوع من List يعمل كـ LIFO (آخر ما يدخل أول ما يخرج)

public class SolitaireGameLogic {
    private Deck deck; // رزمة البطاقات الأصلية
    private Stack<Card> stockPile; // كومة السحب (البطاقات المتبقية)
    private Stack<Card> wastePile; // كومة المهملات (البطاقات المسحوبة)
    private List<Stack<Card>> foundationPiles; // الأكوام الأساسية (4 أكوام لجمع البطاقات حسب النوع)
    private List<List<Card>> tableauPiles; // أكوام اللعب الرئيسية (7 أكوام في وسط اللوحة)

    // منشئ (Constructor) للفئة SolitaireGameLogic
    public SolitaireGameLogic() {
        initializeGame();
    }

    // دالة لتهيئة وبدء لعبة جديدة
    private void initializeGame() {
        deck = new Deck();
        deck.shuffle(); // خلط الرزمة جيداً

        stockPile = new Stack<>();
        wastePile = new Stack<>();
        foundationPiles = new ArrayList<>();
        tableauPiles = new ArrayList<>();

        // تهيئة الأكوام الأساسية الأربعة
        for (int i = 0; i < 4; i++) {
            foundationPiles.add(new Stack<>());
        }

        // تهيئة أكوام اللعب (Tableau Piles) وتوزيع البطاقات عليها
        // كل كومة Tableau تحصل على بطاقة إضافية، والبطاقة الأخيرة تكون مكشوفة
        for (int i = 0; i < 7; i++) {
            tableauPiles.add(new ArrayList<>());
            for (int j = 0; j <= i; j++) {
                Card card = deck.dealCard();
                if (card != null) {
                    card.setFaceUp(j == i); // البطاقة الأخيرة في كل كومة Tableau تكون مكشوفة
                    tableauPiles.get(i).add(card);
                }
            }
        }

        // نقل البطاقات المتبقية إلى كومة السحب (Stock Pile)
        while (!deck.isEmpty()) {
            stockPile.push(deck.dealCard());
        }
    }

    // دالة لسحب بطاقة من كومة السحب (Stock Pile) إلى كومة المهملات (Waste Pile)
    public void drawFromStock() {
        if (!stockPile.isEmpty()) {
            Card card = stockPile.pop();
            card.setFaceUp(true); // البطاقة المسحوبة تصبح مكشوفة
            wastePile.push(card);
        } else {
            // إذا كانت كومة السحب فارغة، انقل بطاقات كومة المهملات إليها (مقلوبة)
            if (wastePile.isEmpty()) {
                // لا توجد بطاقات لسحبها أو إعادة تدويرها
                return;
            }
            while (!wastePile.isEmpty()) {
                Card card = wastePile.pop();
                card.setFaceUp(false); // إعادة قلب البطاقات لوضعها في كومة السحب
                stockPile.push(card);
            }
        }
    }

    // دوال getters للوصول إلى حالة اللعبة (الأكوام المختلفة)
    public Stack<Card> getStockPile() { return stockPile; }
    public Stack<Card> getWastePile() { return wastePile; }
    public List<Stack<Card>> getFoundationPiles() { return foundationPiles; }
    public List<List<Card>> getTableauPiles() { return tableauPiles; }

    // دالة للتحقق من شرط الفوز باللعبة
    public boolean checkWinCondition() {
        // يتم الفوز عندما تكون جميع الأكوام الأساسية الأربعة مكتملة (كل كومة تحتوي على 13 بطاقة)
        for (Stack<Card> foundation : foundationPiles) {
            if (foundation.size() != 13) { // 13 بطاقة من الآس إلى الملك لكل نوع
                return false;
            }
        }
        return true;
    }

    // --- منطق التحقق من صلاحية الحركة (Move Validation Logic) ---

    // التحقق مما إذا كان يمكن نقل بطاقة (أو مجموعة بطاقات) إلى كومة لعب (Tableau Pile)
    public boolean canMoveToTableau(Card cardToMove, List<Card> targetPile) {
        if (!cardToMove.isFaceUp()) {
            return false; // لا يمكن نقل إلا البطاقات المكشوفة إلى أكوام اللعب
        }
        if (targetPile.isEmpty()) {
            return cardToMove.getRank() == Card.Rank.KING; // فقط الملك (King) يمكنه بدء كومة لعب فارغة
        } else {
            Card topCard = targetPile.get(targetPile.size() - 1);
            if (!topCard.isFaceUp()) { // يجب أن تكون البطاقة العلوية في الكومة المستهدفة مكشوفة
                return false;
            }
            // يجب أن تكون رتبة البطاقة المراد نقلها أقل بواحد من رتبة البطاقة العلوية في الكومة المستهدفة
            // (مثال: الملكة على الملك)
            boolean isCorrectRank = (cardToMove.getRank().ordinal() + 1 == topCard.getRank().ordinal());
            // يجب أن تتبادل الألوان (أحمر على أسود أو أسود على أحمر)
            boolean isAlternatingColor = (cardToMove.isRed() && topCard.isBlack()) || (cardToMove.isBlack() && topCard.isRed());
            return isCorrectRank && isAlternatingColor;
        }
    }

    // التحقق مما إذا كان يمكن نقل بطاقة واحدة إلى كومة أساسية (Foundation Pile)
    public boolean canMoveToFoundation(Card cardToMove, Stack<Card> targetFoundation) {
        if (!cardToMove.isFaceUp()) {
            return false; // لا يمكن نقل إلا البطاقات المكشوفة إلى الأكوام الأساسية
        }
        if (targetFoundation.isEmpty()) {
            return cardToMove.getRank() == Card.Rank.ACE; // فقط الآس (Ace) يمكنه بدء كومة أساسية فارغة
        } else {
            Card topCard = targetFoundation.peek(); // البطاقة العلوية في الكومة الأساسية
            boolean isSameSuit = (cardToMove.getSuit() == topCard.getSuit()); // يجب أن تكون من نفس النوع
            // يجب أن تكون رتبة البطاقة المراد نقلها أكبر بواحد من رتبة البطاقة العلوية
            // (مثال: 2 على الآس، 3 على 2، إلخ)
            boolean isCorrectRank = (cardToMove.getRank().ordinal() - 1 == topCard.getRank().ordinal());
            return isSameSuit && isCorrectRank;
        }
    }
    
    // --- دوال نقل البطاقات (Card Movement Methods) ---
    
    // نقل بطاقة واحدة من كومة مصدر إلى كومة وجهة (عامة)
    public void moveCard(Card card, List<Card> source, List<Card> destination) {
        if (source.remove(card)) { // حاول إزالة البطاقة من الكومة المصدر
            destination.add(card); // أضف البطاقة إلى الكومة الوجهة
            // إذا كانت الكومة المصدر هي كومة لعب (Tableau Pile) ولم تعد فارغة، اقلب البطاقة العلوية الجديدة إذا كانت مقلوبة
            if (source instanceof ArrayList && !source.isEmpty()) { // أكوام Tableau هي ArrayLists
                Card newTopCard = ((ArrayList<Card>) source).get(source.size() - 1);
                if (!newTopCard.isFaceUp()) {
                    newTopCard.setFaceUp(true);
                }
            }
        }
    }
    
    // نقل مجموعة من البطاقات من كومة لعب (Tableau Pile) إلى كومة لعب أخرى
    public void moveCards(List<Card> cardsToMove, List<Card> sourceTableau, List<Card> destinationTableau) {
        // تحديد فهرس أول بطاقة في المجموعة المراد نقلها داخل الكومة المصدر
        int startIndex = sourceTableau.indexOf(cardsToMove.get(0));
        if (startIndex == -1) return; // لا ينبغي أن يحدث هذا، يعني أن البطاقة ليست في المصدر

        // إزالة البطاقات من الكومة المصدر
        // يتم إنشاء قائمة مؤقتة لإزالة العناصر بشكل صحيح لتجنب ConcurrentModificationException
        List<Card> removedCards = new ArrayList<>();
        for (int i = startIndex; i < sourceTableau.size(); i++) {
            removedCards.add(sourceTableau.get(i));
        }
        sourceTableau.removeAll(removedCards); // إزالة القائمة الفرعية

        // إضافة البطاقات إلى الكومة الوجهة
        destinationTableau.addAll(removedCards);

        // إذا كانت الكومة المصدر ليست فارغة والبطاقة العلوية الجديدة مقلوبة، اقلبها
        if (!sourceTableau.isEmpty()) {
            Card newTopCard = sourceTableau.get(sourceTableau.size() - 1);
            if (!newTopCard.isFaceUp()) {
                newTopCard.setFaceUp(true);
            }
        }
    }
}



--

4. هيكلة الواجهة الرسومية (GUI)

بعد بناء منطق اللعبة الأساسي، ننتقل الآن إلى إنشاء الواجهة الرسومية (GUI)
 التي سيتفاعل معها المستخدم. سنستخدم مكونات Swing في Java.


4.1. فئة CardImages (أداة تحميل الصور)

هذه الفئة ستقوم بتحميل وإدارة صور البطاقات. يجب عليك تجهيز مجلد resources
 داخل مشروعك في NetBeans، ووضع صور البطاقات فيه.





* لـ CardImages.java (تكملة) :
Java




package solitairegame;

import javax.imageio.ImageIO;
import java.awt.Image;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; // لـ Objects.requireNonNull

public class CardImages {
    private static final String RESOURCE_PATH = "/resources/"; // المسار إلى مجلد الصور
    private static final String FILE_EXTENSION = ".png"; // افتراض أن تنسيق الملفات هو PNG

    // خريطة لتخزين صور البطاقات بحيث يمكن الوصول إليها بسرعة باستخدام مفتاح (مثل "CLUBS_ACE")
    private static Map<String, Image> cardImageMap;
    private static Image cardBackImage; // صورة ظهر البطاقة

    // كتلة تهيئة ثابتة (static initializer block) لتحميل الصور مرة واحدة عند تحميل الفئة
    static {
        cardImageMap = new HashMap<>();
        try {
            // تحميل صورة ظهر البطاقة من المسار المحدد
            cardBackImage = ImageIO.read(Objects.requireNonNull(CardImages.class.getResource(RESOURCE_PATH + "back" + FILE_EXTENSION)));

            // تحميل صور البطاقات الفردية لكل نوع ورتبة
            for (Card.Suit suit : Card.Suit.values()) {
                for (Card.Rank rank : Card.Rank.values()) {
                    String fileName = getCardFileName(suit, rank); // الحصول على اسم الملف بناءً على النوع والرتبة
                    Image img = ImageIO.read(Objects.requireNonNull(CardImages.class.getResource(RESOURCE_PATH + fileName + FILE_EXTENSION)));
                    cardImageMap.put(suit.name() + "_" + rank.name(), img); // تخزين الصورة في الخريطة
                }
            }
        } catch (IOException e) {
            System.err.println("خطأ في تحميل صور البطاقات: " + e.getMessage());
            e.printStackTrace();
            // يمكنك هنا إضافة معالجة أفضل للخطأ، مثل استخدام صور افتراضية بدلاً من ذلك
        }
    }

    // دالة مساعدة لإنشاء اسم ملف الصورة بناءً على نوع ورتبة البطاقة
    private static String getCardFileName(Card.Suit suit, Card.Rank rank) {
        String rankStr = rank.name().toLowerCase(); // تحويل الرتبة إلى حروف صغيرة (مثال: ace -> ace)
        String suitStr = suit.name().toLowerCase(); // تحويل النوع إلى حروف صغيرة (مثال: clubs -> clubs)
        
        // تعديل اسم الملف لرتب معينة لتتناسب مع تسمية الصور الشائعة
        // على سبيل المثال، قد تكون صور الآس مسماة بـ '1'، والجاك بـ 'j'، وهكذا.
        switch (rank) {
            case ACE: rankStr = "1"; break;
            case TWO: rankStr = "2"; break;
            case THREE: rankStr = "3"; break;
            case FOUR: rankStr = "4"; break;
            case FIVE: rankStr = "5"; break;
            case SIX: rankStr = "6"; break;
            case SEVEN: rankStr = "7"; break;
            case EIGHT: rankStr = "8"; break;
            case NINE: rankStr = "9"; break;
            case TEN: rankStr = "10"; break;
            case JACK: rankStr = "j"; break;
            case QUEEN: rankStr = "q"; break;
            case KING: rankStr = "k"; break;
        }

        // بناء اسم الملف (مثال: "s1" لـ Ace of Spades، "dk" لـ King of Diamonds)
        // يجب أن يتطابق هذا مع أسماء الملفات الفعلية لصورك في مجلد 'resources'
        return suitStr.charAt(0) + rankStr; 
    }

    // دالة للحصول على صورة بطاقة معينة (مكشوفة أو مقلوبة)
    public static Image getCardImage(Card card) {
        if (card.isFaceUp()) {
            // إذا كانت البطاقة مكشوفة، أعد الصورة الخاصة بها من الخريطة
            return cardImageMap.get(card.getSuit().name() + "_" + card.getRank().name());
        } else {
            // إذا كانت البطاقة مقلوبة، أعد صورة ظهر البطاقة
            return cardBackImage;
        }
    }

    // دالة للحصول على صورة ظهر البطاقة فقط
    public static Image getCardBackImage() {
        return cardBackImage;
    }
}


--

* ملاحظة هامة حول getCardFileName: يجب أن تتطابق هذه الدالة مع
 نمط تسمية ملفات الصور لديك في مجلد resources. في الكود أعلاه، 
افترضنا أن ملفاتك مسماة بأول حرف من النوع متبوعًا برتبة البطاقة 
(مثل s1.png لـ Ace of Spades، dq.png لـ Queen of Diamonds). 
قم بتعديل getCardFileName لتعكس طريقة تسمية ملفاتك بدقة. على سبيل المثال،
 إذا كان اسم ملف Ace of Spades هو spades_ace.png، فستحتاج إلى تغيير الدالة لتعود بـ suit.name().toLowerCase() + "_" + rank.name().toLowerCase().

4.2. فئة GamePanel (لوحة الرسم الرئيسية للعبة)

ستكون هذه الفئة هي المكون JPanel الذي سنرسم عليه جميع البطاقات والحالات المختلفة للعبة.
إنشاء الفئة: أنشئ فئة جديدة باسم GamePanel.java.

* لـ GamePanel.java:
Java




package solitairegame;

import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.Stack;

public class GamePanel extends JPanel {
    private SolitaireGameLogic game; // كائن منطق اللعبة للوصول إلى حالة اللعبة
    private static final int CARD_WIDTH = 73; // عرض صورة البطاقة
    private static final int CARD_HEIGHT = 98; // ارتفاع صورة البطاقة
    private static final int PADDING = 10; // المسافة البادئة حول العناصر
    private static final int CARD_STACK_OFFSET = 20; // الإزاحة الرأسية للبطاقات المكدسة في أكوام Tableau

    // كائنات Rectangle لتمثيل مناطق كل كومة على الشاشة
    private Rectangle stockRect; // منطقة كومة السحب
    private Rectangle wasteRect; // منطقة كومة المهملات
    private Rectangle[] foundationRects; // مصفوفة لمناطق الأكوام الأساسية الأربعة
    private Rectangle[] tableauRects; // مصفوفة لمناطق أكوام اللعب السبعة

    // منشئ (Constructor) للفئة GamePanel
    public GamePanel(SolitaireGameLogic game) {
        this.game = game;
        this.setPreferredSize(new Dimension(800, 600)); // تعيين الحجم المفضل للوحة
        this.setBackground(new Color(0, 128, 0)); // تعيين لون الخلفية (أخضر كسطح طاولة اللعب)
    }

    // دالة setter لتحديث كائن منطق اللعبة (تستخدم عند بدء لعبة جديدة)
    public void setGame(SolitaireGameLogic game) {
        this.game = game;
    }

    // دالة paintComponent هي المسؤولة عن رسم مكونات JPanel
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g); // استدعاء دالة paintComponent الأصلية لتنظيف اللوحة
        Graphics2D g2d = (Graphics2D) g; // استخدام Graphics2D للحصول على ميزات رسم متقدمة

        // --- إعادة حساب المواقع بناءً على الحجم الحالي للوحة للاستجابة لتغيير حجم النافذة ---
        stockRect = new Rectangle(PADDING, PADDING, CARD_WIDTH, CARD_HEIGHT);
        wasteRect = new Rectangle(PADDING * 2 + CARD_WIDTH, PADDING, CARD_WIDTH, CARD_HEIGHT);

        foundationRects = new Rectangle[4];
        // حساب نقطة بداية أكوام الأساسيات من اليمين للحفاظ على التنسيق
        int startXFoundation = getWidth() - (4 * CARD_WIDTH + 3 * PADDING);
        for (int i = 0; i < 4; i++) {
            foundationRects[i] = new Rectangle(startXFoundation + (i * (CARD_WIDTH + PADDING)), PADDING, CARD_WIDTH, CARD_HEIGHT);
        }

        tableauRects = new Rectangle[7];
        int startYTableau = PADDING * 2 + CARD_HEIGHT; // بداية أكوام Tableau تحت الأكوام العلوية
        for (int i = 0; i < 7; i++) {
            tableauRects[i] = new Rectangle(PADDING + (i * (CARD_WIDTH + PADDING)), startYTableau, CARD_WIDTH, CARD_HEIGHT);
        }
        // --- انتهاء إعادة الحساب ---

        // رسم كومة السحب (Stock Pile)
        if (game.getStockPile().isEmpty()) {
            g2d.setColor(Color.DARK_GRAY);
            g2d.drawRect(stockRect.x, stockRect.y, stockRect.width, stockRect.height); // ارسم مستطيلاً فارغاً
        } else {
            g2d.drawImage(CardImages.getCardBackImage(), stockRect.x, stockRect.y, CARD_WIDTH, CARD_HEIGHT, this); // ارسم ظهر البطاقة
        }

        // رسم كومة المهملات (Waste Pile)
        if (game.getWastePile().isEmpty()) {
            g2d.setColor(Color.DARK_GRAY);
            g2d.drawRect(wasteRect.x, wasteRect.y, wasteRect.width, wasteRect.height); // ارسم مستطيلاً فارغاً
        } else {
            // ارسم فقط البطاقة العلوية من كومة المهملات
            Card topCard = game.getWastePile().peek();
            g2d.drawImage(CardImages.getCardImage(topCard), wasteRect.x, wasteRect.y, CARD_WIDTH, CARD_HEIGHT, this);
        }

        // رسم الأكوام الأساسية (Foundation Piles)
        for (int i = 0; i < 4; i++) {
            Stack<Card> foundation = game.getFoundationPiles().get(i);
            if (foundation.isEmpty()) {
                g2d.setColor(Color.DARK_GRAY);
                g2d.drawRect(foundationRects[i].x, foundationRects[i].y, foundationRects[i].width, foundationRects[i].height); // ارسم مستطيلاً فارغاً
            } else {
                Card topCard = foundation.peek();
                g2d.drawImage(CardImages.getCardImage(topCard), foundationRects[i].x, foundationRects[i].y, CARD_WIDTH, CARD_HEIGHT, this);
            }
        }

        // رسم أكوام اللعب (Tableau Piles)
        for (int i = 0; i < 7; i++) {
            List<Card> tableau = game.getTableauPiles().get(i);
            if (tableau.isEmpty()) {
                g2d.setColor(Color.DARK_GRAY);
                g2d.drawRect(tableauRects[i].x, tableauRects[i].y, tableauRects[i].width, tableauRects[i].height); // ارسم مستطيلاً فارغاً
            } else {
                for (int j = 0; j < tableau.size(); j++) {
                    Card card = tableau.get(j);
                    int x = tableauRects[i].x;
                    int y = tableauRects[i].y + j * CARD_STACK_OFFSET; // إزاحة عمودية للبطاقات المكدسة
                    g2d.drawImage(CardImages.getCardImage(card), x, y, CARD_WIDTH, CARD_HEIGHT, this);

                    // إذا كانت البطاقة مقلوبة، ارسم مستطيلاً داكناً فوقها لتوضيح ذلك
                    if (!card.isFaceUp()) {
                        g2d.setColor(new Color(0, 0, 0, 100)); // أسود شبه شفاف
                        g2d.fillRect(x, y, CARD_WIDTH, CARD_HEIGHT);
                    }
                }
            }
        }
    }

    // دوال getters لكائنات Rectangle لتمكين MouseListeners من تحديد مناطق النقر
    public Rectangle getStockRect() { return stockRect; }
    public Rectangle getWasteRect() { return wasteRect; }
    public Rectangle[] getFoundationRects() { return foundationRects; }
    public Rectangle[] getTableauRects() { return tableauRects; }
}


--

4.3. تعديل SolitaireGame (الفئة الرئيسية لـ JFrame)

الآن سنقوم بتعديل الفئة الرئيسية SolitaireGame.java 
(التي أنشأها NetBeans) لتكون JFrame (إطار النافذة الرئيسي) و
تستضيف GamePanel (لوحة اللعبة). كما سنضيف إليها منطق التعامل مع أحداث الماوس.

* تعديل SolitaireGame.java:
Java




package solitairegame;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections; // تم تضمينها لتسهيل التعامل مع قوائم البطاقات
import java.util.List;
import java.util.Stack;

public class SolitaireGame extends JFrame { // تم التغيير لتوسيع JFrame
    private GamePanel gamePanel; // لوحة اللعبة الرسومية
    private SolitaireGameLogic gameLogic; // كائن منطق اللعبة

    // أبعاد البطاقات والمسافات (يمكن جعلها ديناميكية لاحقاً)
    private static final int CARD_WIDTH = 73;
    private static final int CARD_HEIGHT = 98;
    private static final int PADDING = 10;
    private static final int CARD_STACK_OFFSET = 20;

    // --- متغيرات السحب والإفلات (Drag and Drop) ---
    private Card draggedCard; // البطاقة التي يتم سحبها حالياً
    private List<Card> draggedCardsPile; // مجموعة البطاقات التي يتم سحبها (في حالة Tableau)
    private Point dragOffset; // إزاحة مؤشر الماوس عن الزاوية العلوية اليسرى للبطاقة عند النقر
    private List<Card> sourcePile; // الكومة الأصلية التي سحبت منها البطاقة/البطاقات

    // منشئ (Constructor) للفئة SolitaireGame
    public SolitaireGame() {
        super("Solitaire by YourName"); // عنوان النافذة
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // إغلاق التطبيق عند إغلاق النافذة
        setResizable(true); // السماح بتغيير حجم النافذة
        
        gameLogic = new SolitaireGameLogic(); // تهيئة منطق اللعبة
        gamePanel = new GamePanel(gameLogic); // تمرير منطق اللعبة إلى لوحة الرسم

        add(gamePanel, BorderLayout.CENTER); // إضافة لوحة اللعبة إلى منتصف الإطار

        // إضافة شريط القائمة (Menu Bar)
        JMenuBar menuBar = new JMenuBar();
        JMenu gameMenu = new JMenu("Game"); // قائمة "اللعبة"
        JMenuItem newGameItem = new JMenuItem("New Game"); // عنصر "لعبة جديدة"
        newGameItem.addActionListener(e -> startNewGame()); // عند النقر، ابدأ لعبة جديدة
        JMenuItem exitItem = new JMenuItem("Exit"); // عنصر "خروج"
        exitItem.addActionListener(e -> System.exit(0)); // عند النقر، أغلق التطبيق
        gameMenu.add(newGameItem);
        gameMenu.addSeparator(); // فاصل
        gameMenu.add(exitItem);
        menuBar.add(gameMenu);
        setJMenuBar(menuBar); // تعيين شريط القائمة للإطار

        // إضافة مستمعي الماوس للتفاعل مع اللعبة
        addMouseListeners();

        pack(); // تجميع المكونات لضبط حجم الإطار حسب الحجم المفضل للوحة
        setLocationRelativeTo(null); // توسيط النافذة على الشاشة
        setVisible(true); // إظهار النافذة
    }

    // دالة لبدء لعبة جديدة
    private void startNewGame() {
        gameLogic = new SolitaireGameLogic(); // إعادة تهيئة منطق اللعبة
        gamePanel.setGame(gameLogic); // تحديث كائن منطق اللعبة في لوحة الرسم
        repaint(); // إعادة رسم اللوحة بأكملها
    }

    // --- تطبيق مستمعي الماوس (Mouse Listener Implementation) ---
    private void addMouseListeners() {
        MouseAdapter mouseAdapter = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (draggedCard != null) return; // لا تفعل شيئاً إذا كانت بطاقة ما يتم سحبها بالفعل

                Point clickPoint = e.getPoint(); // نقطة النقر

                // 1. التحقق من النقر على كومة المهملات (Waste Pile)
                if (!gameLogic.getWastePile().isEmpty() && gamePanel.getWasteRect().contains(clickPoint)) {
                    draggedCard = gameLogic.getWastePile().peek(); // البطاقة العلوية هي التي ستسحب
                    draggedCardsPile = new ArrayList<>();
                    draggedCardsPile.add(draggedCard); // إضافة البطاقة إلى قائمة البطاقات المسحوبة
                    sourcePile = gameLogic.getWastePile(); // مصدر السحب هو كومة المهملات
                    // حساب إزاحة الماوس عن البطاقة
                    dragOffset = new Point(clickPoint.x - gamePanel.getWasteRect().x, clickPoint.y - gamePanel.getWasteRect().y);
                    return; // تم التعامل مع النقر
                }

                // 2. التحقق من النقر على أكوام اللعب (Tableau Piles)
                for (int i = 0; i < 7; i++) {
                    List<Card> tableau = gameLogic.getTableauPiles().get(i);
                    if (!tableau.isEmpty()) {
                        // التكرار من الأسفل (البطاقة العلوية) إلى الأعلى للعثور على البطاقة المنقورة المكشوفة
                        for (int j = tableau.size() - 1; j >= 0; j--) {
                            Card card = tableau.get(j);
                            if (card.isFaceUp()) { // يمكن سحب البطاقات المكشوفة فقط
                                // حساب منطقة البطاقة المنقورة
                                Rectangle cardRect = new Rectangle(
                                        gamePanel.getTableauRects()[i].x,
                                        gamePanel.getTableauRects()[i].y + j * CARD_STACK_OFFSET,
                                        CARD_WIDTH, CARD_HEIGHT);
                                if (cardRect.contains(clickPoint)) {
                                    // تم العثور على البطاقة العلوية المكشوفة التي تم النقر عليها (أو جزء من مجموعة سحب)
                                    // الحصول على جميع البطاقات من البطاقة المنقورة وحتى نهاية الكومة
                                    draggedCardsPile = new ArrayList<>(tableau.subList(j, tableau.size()));
                                    draggedCard = draggedCardsPile.get(0); // البطاقة الأولى في المجموعة المسحوبة
                                    sourcePile = tableau; // الكومة المصدر هي كومة Tableau الحالية
                                    // حساب إزاحة الماوس
                                    dragOffset = new Point(clickPoint.x - cardRect.x, clickPoint.y - cardRect.y);
                                    return; // تم التعامل مع النقر
                                }
                            }
                        }
                    }
                }

                // 3. التحقق من النقر على أكوام الأساسيات (Foundation Piles)
                for (int i = 0; i < 4; i++) {
                    Stack<Card> foundation = gameLogic.getFoundationPiles().get(i);
                    if (!foundation.isEmpty()) {
                        Rectangle foundationRect = gamePanel.getFoundationRects()[i];
                        if (foundationRect.contains(clickPoint)) {
                            draggedCard = foundation.peek(); // يمكن سحب البطاقة العلوية فقط
                            draggedCardsPile = new ArrayList<>();
                            draggedCardsPile.add(draggedCard);
                            sourcePile = foundation; // المصدر هو كومة الأساسيات
                            dragOffset = new Point(clickPoint.x - foundationRect.x, clickPoint.y - foundationRect.y);
                            return; // تم التعامل مع النقر
                        }
                    }
                }
            }

            @Override
            public void mouseDragged(MouseEvent e) {
                if (draggedCard != null) {
                    // ببساطة أعد رسم اللوحة بالكامل لتحديث موضع البطاقة المسحوبة
                    repaint(); 
                }
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (draggedCard == null) return; // لا توجد بطاقة تم سحبها

                Point dropPoint = e.getPoint(); // نقطة الإفلات
                boolean moveSuccessful = false;

                // 1. محاولة النقل إلى أكوام الأساسيات (Foundation Piles)
                for (int i = 0; i < 4; i++) {
                    if (gamePanel.getFoundationRects()[i].contains(dropPoint) && draggedCardsPile.size() == 1) { // يمكن نقل بطاقة واحدة فقط إلى الأساسيات
                        if (gameLogic.canMoveToFoundation(draggedCard, gameLogic.getFoundationPiles().get(i))) {
                            gameLogic.moveCard(draggedCard, sourcePile, gameLogic.getFoundationPiles().get(i));
                            moveSuccessful = true;
                            break; // تم النقل بنجاح
                        }
                    }
                }

                // 2. محاولة النقل إلى أكوام اللعب (Tableau Piles) (فقط إذا لم يتم النقل إلى الأساسيات)
                if (!moveSuccessful) {
                    for (int i = 0; i < 7; i++) {
                        // تحديد منطقة الإفلات المستهدفة لكومة Tableau
                        // يمكن أن تكون منطقة البطاقة العلوية أو منطقة القاعدة إذا كانت فارغة
                        Rectangle targetArea = new Rectangle(gamePanel.getTableauRects()[i].x, gamePanel.getTableauRects()[i].y, CARD_WIDTH, CARD_HEIGHT);
                        if (!gameLogic.getTableauPiles().get(i).isEmpty()) {
                             // تقدير تقريبي لمنطقة الإفلات للكومة لتشمل التراص
                             targetArea = new Rectangle(
                                 gamePanel.getTableauRects()[i].x,
                                 gamePanel.getTableauRects()[i].y + (gameLogic.getTableauPiles().get(i).size() - 1) * CARD_STACK_OFFSET,
                                 CARD_WIDTH, CARD_HEIGHT + (gameLogic.getTableauPiles().get(i).size() - 1) * CARD_STACK_OFFSET
                            );
                        }

                        if (targetArea.contains(dropPoint)) {
                            if (gameLogic.canMoveToTableau(draggedCardsPile.get(0), gameLogic.getTableauPiles().get(i))) {
                                gameLogic.moveCards(draggedCardsPile, sourcePile, gameLogic.getTableauPiles().get(i));
                                moveSuccessful = true;
                                break; // تم النقل بنجاح
                            }
                        }
                    }
                }
                
                // بعد محاولة النقل، أعد تعيين متغيرات السحب
                draggedCard = null;
                draggedCardsPile = null;
                sourcePile = null;
                dragOffset = null;
                
                repaint(); // أعد رسم اللوحة بعد الحركة المحتملة
                checkWin(); // تحقق من شرط الفوز بعد كل حركة
            }

            @Override
            public void mouseClicked(MouseEvent e) {
                Point clickPoint = e.getPoint();

                // النقر على كومة السحب (Stock Pile)
                if (gamePanel.getStockRect().contains(clickPoint) && e.getButton() == MouseEvent.BUTTON1) {
                    gameLogic.drawFromStock(); // اسحب بطاقة
                    repaint(); // أعد الرسم لتحديث الواجهة
                }
                // النقر المزدوج (Double-click) لنقل البطاقة تلقائياً إلى الأساسيات
                else if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
                    autoMoveToFoundation(clickPoint); // محاولة نقل البطاقة تلقائياً
                }
            }
        };
        gamePanel.addMouseListener(mouseAdapter); // إضافة مستمع الماوس للوحة
        gamePanel.addMouseMotionListener(mouseAdapter); // إضافة مستمع حركة الماوس (لأحداث السحب)
    }

    // دالة لمساعجة نقل البطاقة تلقائياً إلى كومة الأساسيات عند النقر المزدوج
    private void autoMoveToFoundation(Point clickPoint) {
        Card clickedCard = null;
        List<Card> source = null;

        // التحقق مما إذا تم النقر على كومة المهملات (Waste Pile)
        if (!gameLogic.getWastePile().isEmpty() && gamePanel.getWasteRect().contains(clickPoint)) {
            clickedCard = gameLogic.getWastePile().peek();
            source = gameLogic.getWastePile();
        }

        // التحقق مما إذا تم النقر على أكوام اللعب (Tableau Piles)
        if (clickedCard == null) {
            for (int i = 0; i < 7; i++) {
                List<Card> tableau = gameLogic.getTableauPiles().get(i);
                if (!tableau.isEmpty()) {
                    // تحديد منطقة البطاقة العلوية في كومة Tableau
                    Rectangle topCardRect = new Rectangle(
                            gamePanel.getTableauRects()[i].x,
                            gamePanel.getTableauRects()[i].y + (tableau.size() - 1) * CARD_STACK_OFFSET,
                            CARD_WIDTH, CARD_HEIGHT);
                    // إذا تم النقر عليها وكانت مكشوفة
                    if (topCardRect.contains(clickPoint) && tableau.get(tableau.size() - 1).isFaceUp()) {
                        clickedCard = tableau.get(tableau.size() - 1);
                        source = tableau;
                        break;
                    }
                }
            }
        }

        // إذا تم العثور على بطاقة منقورة ومصدرها
        if (clickedCard != null && source != null) {
            boolean moved = false;
            // حاول نقلها إلى أي من أكوام الأساسيات الأربعة
            for (int i = 0; i < 4; i++) {
                Stack<Card> foundation = gameLogic.getFoundationPiles().get(i);
                if (gameLogic.canMoveToFoundation(clickedCard, foundation)) {
                    // إزالة البطاقة من المصدر (Stack أو ArrayList)
                    if (source instanceof Stack) { 
                        ((Stack<Card>) source).pop();
                    } else if (source instanceof ArrayList) { 
                        ((ArrayList<Card>) source).remove(clickedCard);
                        // إذا كانت الكومة المصدر (Tableau) لم تعد فارغة، اقلب البطاقة العلوية الجديدة
                        if (!source.isEmpty()) {
                            Card newTopCard = ((ArrayList<Card>) source).get(source.size() - 1);
                            if (!newTopCard.isFaceUp()) {
                                newTopCard.setFaceUp(true);
                            }
                        }
                    }
                    foundation.push(clickedCard); // أضف البطاقة إلى كومة الأساسيات
                    moved = true;
                    break;
                }
            }
            if (moved) {
                repaint(); // أعد الرسم بعد النقل التلقائي
                checkWin(); // تحقق من الفوز
            }
        }
    }

    // دالة paint مُعادة التعريف لرسم البطاقة المسحوبة فوق جميع المكونات الأخرى
    @Override
    public void paint(Graphics g) {
        super.paint(g); // ارسم لوحة اللعبة الرئيسية أولاً
        Graphics2D g2d = (Graphics2D) g;

        // إذا كانت هناك بطاقة يتم سحبها حالياً
        if (draggedCard != null && dragOffset != null) {
            // احصل على موضع الماوس الحالي بالنسبة للشاشة، ثم حوله إلى إحداثيات الإطار
            Point mousePos = MouseInfo.getPointerInfo().getLocation();
            SwingUtilities.convertPointFromScreen(mousePos, this); 

            // احسب موضع رسم البطاقة المسحوبة بناءً على موضع الماوس والإزاحة الأصلية
            int currentX = mousePos.x - dragOffset.x;
            int currentY = mousePos.y - dragOffset.y;

            // ارسم جميع البطاقات المسحوبة (لأكوام Tableau)
            for (int i = 0; i < draggedCardsPile.size(); i++) {
                Card card = draggedCardsPile.get(i);
                g2d.drawImage(CardImages.getCardImage(card), currentX, currentY + i * CARD_STACK_OFFSET, CARD_WIDTH, CARD_HEIGHT, this);
            }
        }
    }

    // دالة للتحقق من شرط الفوز وعرض رسالة
    private void checkWin() {
        if (gameLogic.checkWinCondition()) {
            JOptionPane.showMessageDialog(this, "تهانينا! لقد فزت بلعبة السوليتير!", "اللعبة انتهت", JOptionPane.INFORMATION_MESSAGE);
            startNewGame(); // ابدأ لعبة جديدة تلقائياً
        }
    }

    // الدالة الرئيسية لتشغيل اللعبة
    public static void main(String[] args) {
        // تأكد من تنفيذ تحديثات واجهة المستخدم الرسومية على Event Dispatch Thread (EDT)
        SwingUtilities.invokeLater(() -> {
            new SolitaireGame(); // إنشاء وتشغيل نافذة اللعبة
        });
    }
}



--

* ملاحظة : لقد قمت بإعادة تسمية الفئة SolitaireGame 
(التي تحتوي على منطق اللعبة) إلى SolitaireGameLogic لجعل الأمور أكثر وضوحًا. 
يجب عليك إنشاء ملف Java جديد باسم SolitaireGameLogic.java 
ولصق الكود من القسم 3.3 بداخله، مع التأكد من أن اسم الفئة في الملف الجديد هو
 public class SolitaireGameLogic.




5. التعامل مع تفاعلات المستخدم (أحداث الماوس)

لجعل اللعبة قابلة للعب، نحتاج إلى معالجة نقرات الماوس والسحب والإفلات. 
سنضيف MouseAdapter إلى GamePanel للكشف عن هذه الأحداث.

5.1. إضافة منطق الماوس إلى GamePanel
سنقوم بتعديل دالة addMouseListeners() في فئة SolitaireGame (الفئة الرئيسية التي تمثل JFrame).

* لـ SolitaireGame.java (تكملة في ()addMouseListeners):
Java




// ... (داخل فئة SolitaireGame)

    private void addMouseListeners() {
        MouseAdapter mouseAdapter = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (draggedCard != null) return; // Already dragging

                Point clickPoint = e.getPoint();

                // 1. Check Waste Pile
                if (!gameLogic.getWastePile().isEmpty() && wasteRect.contains(clickPoint)) {
                    draggedCard = gameLogic.getWastePile().peek();
                    draggedCardsPile = new ArrayList<>();
                    draggedCardsPile.add(draggedCard);
                    sourcePile = gameLogic.getWastePile();
                    dragOffset = new Point(clickPoint.x - wasteRect.x, clickPoint.y - wasteRect.y);
                    originalSourceRect = new Rectangle(wasteRect); // Save original pos
                    return;
                }

                // 2. Check Tableau Piles
                for (int i = 0; i < 7; i++) {
                    List<Card> tableau = gameLogic.getTableauPiles().get(i);
                    if (!tableau.isEmpty()) {
                        for (int j = tableau.size() - 1; j >= 0; j--) {
                            Card card = tableau.get(j);
                            if (card.isFaceUp()) { // Only face-up cards can be dragged
                                Rectangle cardRect = new Rectangle(
                                        tableauRects[i].x,
                                        tableauRects[i].y + j * CARD_STACK_OFFSET,
                                        CARD_WIDTH, CARD_HEIGHT);
                                if (cardRect.contains(clickPoint)) {
                                    // Found the top-most face-up card clicked
                                    draggedCardsPile = new ArrayList<>(tableau.subList(j, tableau.size()));
                                    draggedCard = draggedCardsPile.get(0);
                                    sourcePile = tableau; // Keep reference to original tableau pile
                                    dragOffset = new Point(clickPoint.x - cardRect.x, clickPoint.y - cardRect.y);
                                    originalSourceRect = new Rectangle(cardRect);
                                    return;
                                }
                            }
                        }
                    }
                }

                // 3. Check Foundation Piles (only top card can be dragged)
                for (int i = 0; i < 4; i++) {
                    Stack<Card> foundation = gameLogic.getFoundationPiles().get(i);
                    if (!foundation.isEmpty()) {
                        Rectangle foundationRect = foundationRects[i];
                        if (foundationRect.contains(clickPoint)) {
                            draggedCard = foundation.peek();
                            draggedCardsPile = new ArrayList<>();
                            draggedCardsPile.add(draggedCard);
                            sourcePile = foundation; // Source is foundation
                            dragOffset = new Point(clickPoint.x - foundationRect.x, clickPoint.y - foundationRect.y);
                            originalSourceRect = new Rectangle(foundationRect);
                            return;
                        }
                    }
                }
            }

            @Override
            public void mouseDragged(MouseEvent e) {
                if (draggedCard != null) {
                    // Update the position of the dragged card (for single card)
                    // For multiple cards, update the whole pile
                    int currentX = e.getX() - dragOffset.x;
                    int currentY = e.getY() - dragOffset.y;

                    // Repaint the area where the cards were and where they are now
                    repaint(); // Simplest way, repaints the whole panel
                }
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (draggedCard == null) return; // No card was dragged

                Point dropPoint = e.getPoint();
                boolean moveSuccessful = false;

                // 1. Try to move to Foundation Piles
                for (int i = 0; i < 4; i++) {
                    if (foundationRects[i].contains(dropPoint) && draggedCardsPile.size() == 1) { // Only single cards to foundation
                        if (gameLogic.canMoveToFoundation(draggedCard, gameLogic.getFoundationPiles().get(i))) {
                            gameLogic.moveCard(draggedCard, sourcePile, gameLogic.getFoundationPiles().get(i));
                            moveSuccessful = true;
                            break;
                        }
                    }
                }

                // 2. Try to move to Tableau Piles (only if not already moved to foundation)
                if (!moveSuccessful) {
                    for (int i = 0; i < 7; i++) {
                        // Determine the drop target area for the tableau pile
                        // This can be the top card's rectangle or the pile base if empty
                        Rectangle targetArea = new Rectangle(tableauRects[i].x, tableauRects[i].y, CARD_WIDTH, CARD_HEIGHT);
                        if (!gameLogic.getTableauPiles().get(i).isEmpty()) {
                             targetArea = new Rectangle(
                                tableauRects[i].x,
                                tableauRects[i].y + (gameLogic.getTableauPiles().get(i).size() - 1) * CARD_STACK_OFFSET,
                                CARD_WIDTH, CARD_HEIGHT + (gameLogic.getTableauPiles().get(i).size()) * CARD_STACK_OFFSET
                            ); // This is a rough estimation of pile drop area
                        }


                        if (targetArea.contains(dropPoint)) {
                            if (gameLogic.canMoveToTableau(draggedCardsPile.get(0), gameLogic.getTableauPiles().get(i))) {
                                gameLogic.moveCards(draggedCardsPile, sourcePile, gameLogic.getTableauPiles().get(i));
                                moveSuccessful = true;
                                break;
                            }
                        }
                    }
                }
                
                // If move wasn't successful, return cards to original position (handled by resetting draggedCard)
                draggedCard = null;
                draggedCardsPile = null;
                sourcePile = null;
                dragOffset = null;
                originalSourceRect = null;
                
                repaint(); // Redraw the board after potential move
                checkWin(); // Check for win condition after every move
            }

            @Override
            public void mouseClicked(MouseEvent e) {
                Point clickPoint = e.getPoint();

                // Click on Stock Pile
                if (stockRect.contains(clickPoint) && e.getButton() == MouseEvent.BUTTON1) {
                    gameLogic.drawFromStock();
                    repaint();
                }
                // Double-click to auto-move to foundation
                else if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
                    autoMoveToFoundation(clickPoint);
                }
            }
        };
        gamePanel.addMouseListener(mouseAdapter);
        gamePanel.addMouseMotionListener(mouseAdapter);
    }

    // This method needs to be added to SolitaireGame (JFrame)
    private void autoMoveToFoundation(Point clickPoint) {
        Card clickedCard = null;
        List<Card> source = null;

        // Check Waste Pile
        if (!gameLogic.getWastePile().isEmpty() && wasteRect.contains(clickPoint)) {
            clickedCard = gameLogic.getWastePile().peek();
            source = gameLogic.getWastePile();
        }

        // Check Tableau Piles
        if (clickedCard == null) {
            for (int i = 0; i < 7; i++) {
                List<Card> tableau = gameLogic.getTableauPiles().get(i);
                if (!tableau.isEmpty()) {
                    Rectangle topCardRect = new Rectangle(
                            tableauRects[i].x,
                            tableauRects[i].y + (tableau.size() - 1) * CARD_STACK_OFFSET,
                            CARD_WIDTH, CARD_HEIGHT);
                    if (topCardRect.contains(clickPoint) && tableau.get(tableau.size() - 1).isFaceUp()) {
                        clickedCard = tableau.get(tableau.size() - 1);
                        source = tableau;
                        break;
                    }
                }
            }
        }

        if (clickedCard != null && source != null) {
            boolean moved = false;
            for (int i = 0; i < 4; i++) {
                Stack<Card> foundation = gameLogic.getFoundationPiles().get(i);
                if (gameLogic.canMoveToFoundation(clickedCard, foundation)) {
                    if (source instanceof Stack) { // From waste or another stack
                        ((Stack<Card>) source).pop();
                    } else if (source instanceof List) { // From tableau
                        ((List<Card>) source).remove(clickedCard);
                    }
                    foundation.push(clickedCard);
                    moved = true;
                    break;
                }
            }
            if (moved) {
                repaint();
                checkWin();
            }
        }
    }

    // Override paint to draw dragged card (overlay)
    @Override
    public void paint(Graphics g) {
        super.paint(g); // Paint the main game panel
        Graphics2D g2d = (Graphics2D) g;

        if (draggedCard != null && dragOffset != null) {
            int currentX = MouseInfo.getPointerInfo().getLocation().x - getLocationOnScreen().x - dragOffset.x;
            int currentY = MouseInfo.getPointerInfo().getLocation().y - getLocationOnScreen().y - dragOffset.y;

            // Draw all dragged cards (for tableau piles)
            for (int i = 0; i < draggedCardsPile.size(); i++) {
                Card card = draggedCardsPile.get(i);
                g2d.drawImage(CardImages.getCardImage(card), currentX, currentY + i * CARD_STACK_OFFSET, CARD_WIDTH, CARD_HEIGHT, this);
            }
        }
    }

    private void checkWin() {
        if (gameLogic.checkWinCondition()) {
            JOptionPane.showMessageDialog(this, "Congratulations! You won Solitaire!", "Game Over", JOptionPane.INFORMATION_MESSAGE);
            startNewGame(); // Automatically start a new game
        }
    }



--

6. تطبيق قواعد لعبة سوليتير

لقد بدأنا بالفعل في تضمين بعض منطق قواعد اللعبة في فئة 
SolitaireGameLogic (التي كانت سابقاً SolitaireGame قبل إعادة الهيكلة). 
الآن سنكمل الدالات التي تسمح بنقل البطاقات فعليًا وتطبيق القواعد بشكل كامل.

* تعديل SolitaireGameLogic.java (إضافة دوال الحركة):
Java




package solitairegame;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;

public class SolitaireGameLogic {
    private Deck deck;
    private Stack<Card> stockPile;
    private Stack<Card> wastePile;
    private List<Stack<Card>> foundationPiles;
    private List<List<Card>> tableauPiles;

    public SolitaireGameLogic() {
        initializeGame();
    }

    private void initializeGame() {
        deck = new Deck();
        deck.shuffle();

        stockPile = new Stack<>();
        wastePile = new Stack<>();
        foundationPiles = new ArrayList<>();
        tableauPiles = new ArrayList<>();

        for (int i = 0; i < 4; i++) {
            foundationPiles.add(new Stack<>());
        }

        for (int i = 0; i < 7; i++) {
            tableauPiles.add(new ArrayList<>());
            for (int j = 0; j <= i; j++) {
                Card card = deck.dealCard();
                if (card != null) {
                    card.setFaceUp(j == i);
                    tableauPiles.get(i).add(card);
                }
            }
        }

        while (!deck.isEmpty()) {
            stockPile.push(deck.dealCard());
        }
    }

    public void drawFromStock() {
        if (!stockPile.isEmpty()) {
            Card card = stockPile.pop();
            card.setFaceUp(true);
            wastePile.push(card);
        } else {
            // Reset waste pile to stock if stock is empty
            while (!wastePile.isEmpty()) {
                Card card = wastePile.pop();
                card.setFaceUp(false); // Turn cards face down again
                stockPile.push(card);
            }
        }
    }

    // Getters remain the same
    public Stack<Card> getStockPile() { return stockPile; }
    public Stack<Card> getWastePile() { return wastePile; }
    public List<Stack<Card>> getFoundationPiles() { return foundationPiles; }
    public List<List<Card>> getTableauPiles() { return tableauPiles; }

    public boolean checkWinCondition() {
        for (Stack<Card> foundation : foundationPiles) {
            if (foundation.size() != 13) { // 13 cards = Ace to King
                return false;
            }
        }
        return true;
    }

    // --- Move Validation Logic ---

    // Check if a single card can be moved to a tableau pile
    public boolean canMoveToTableau(Card cardToMove, List<Card> targetPile) {
        if (targetPile.isEmpty()) {
            return cardToMove.getRank() == Card.Rank.KING;
        } else {
            Card topCard = targetPile.get(targetPile.size() - 1);
            if (!topCard.isFaceUp()) {
                return false; // Cannot place on a face-down card
            }
            // Rank must be one less than top card (e.g., Q on K)
            boolean isCorrectRank = (cardToMove.getRank().ordinal() + 1 == topCard.getRank().ordinal());
            // Colors must alternate
            boolean isAlternatingColor = (cardToMove.isRed() && topCard.isBlack()) || (cardToMove.isBlack() && topCard.isRed());
            return isCorrectRank && isAlternatingColor;
        }
    }

    // Check if a single card can be moved to a foundation pile
    public boolean canMoveToFoundation(Card cardToMove, Stack<Card> targetFoundation) {
        if (targetFoundation.isEmpty()) {
            return cardToMove.getRank() == Card.Rank.ACE;
        } else {
            Card topCard = targetFoundation.peek();
            boolean isSameSuit = (cardToMove.getSuit() == topCard.getSuit());
            boolean isCorrectRank = (cardToMove.getRank().ordinal() - 1 == topCard.getRank().ordinal());
            return isSameSuit && isCorrectRank;
        }
    }
    
    // --- Card Movement Methods ---
    
    // Move a single card from one pile to another (generic)
    public void moveCard(Card card, List<Card> source, List<Card> destination) {
        source.remove(card);
        destination.add(card);
        // If the source was a tableau pile and the top card is now face down, flip it
        if (source instanceof ArrayList && !source.isEmpty()) { // Tableau piles are ArrayLists
            Card newTopCard = ((ArrayList<Card>) source).get(source.size() - 1);
            if (!newTopCard.isFaceUp()) {
                newTopCard.setFaceUp(true);
            }
        }
    }
    
    // Move a stack of cards from one tableau pile to another
    public void moveCards(List<Card> cardsToMove, List<Card> sourceTableau, List<Card> destinationTableau) {
        int startIndex = sourceTableau.indexOf(cardsToMove.get(0));
        if (startIndex == -1) return; // Should not happen

        // Remove cards from source
        for (int i = 0; i < cardsToMove.size(); i++) {
            sourceTableau.remove(startIndex); // Remove from the same index as elements shift
        }
        
        // Add cards to destination
        destinationTableau.addAll(cardsToMove);

        // If the source tableau pile is not empty and its new top card is face down, flip it
        if (!sourceTableau.isEmpty()) {
            Card newTopCard = sourceTableau.get(sourceTableau.size() - 1);
            if (!newTopCard.isFaceUp()) {
                newTopCard.setFaceUp(true);
            }
        }
    }
}



--

7. إضافة ميزات اللعبة

لقد قمنا بتضمين ميزة "New Game" (لعبة جديدة) في JMenuBar و
"Winning Condition" (شرط الفوز) في نهاية حركة الماوس. يمكن إضافة المزيد من الميزات.

7.1. إعادة تشغيل اللعبة (startNewGame())
تم تنفيذ هذا بالفعل في الفئة SolitaireGame (نافذة اللعبة الرئيسية).
Java

// ... (داخل فئة SolitaireGame)
    private void startNewGame() {
        gameLogic = new SolitaireGameLogic(); // Re-initialize game logic
        gamePanel.setGame(gameLogic); // Update game logic in the panel (you need to add setGame() to GamePanel)
        repaint(); // Redraw the entire board
    }
// ...
--

- ملاحظة: أضف دالة setGame إلى فئة GamePanel.java:
Java

// ... (داخل فئة GamePanel)
    public void setGame(SolitaireGameLogic game) { // Change SolitaireGame to SolitaireGameLogic
        this.game = game;
    }
// ...
--

7.2. التحقق من شرط الفوز (checkWin())
يتم استدعاء هذه الدالة بعد كل حركة ناجحة للتحقق مما إذا كان اللاعب قد فاز.
Java

// ... (داخل فئة SolitaireGame)
    private void checkWin() {
        if (gameLogic.checkWinCondition()) {
            JOptionPane.showMessageDialog(this, "Congratulations! You won Solitaire!", "Game Over", JOptionPane.INFORMATION_MESSAGE);
            startNewGame(); // Automatically start a new game
        }
    }
// ...
--

8. تشغيل واختبار لعبتك

بعد إكمال جميع الفئات والأكواد، حان الوقت لتشغيل واختبار لعبتك.
- الخطوات:

- بناء المشروع: في NetBeans، انقر بزر الماوس الأيمن على مشروع
 SolitaireGame في نافذة Projects، ثم اختر Clean and Build. 
سيتحقق هذا من وجود أي أخطاء في بناء الكود.
- تشغيل اللعبة: انقر بزر الماوس الأيمن على مشروع SolitaireGame، ثم اختر Run.
- الاختبار: تأكد من ظهور نافذة اللعبة مع تخطيط البطاقات الصحيح.
- جرب النقر على رزمة السحب لسحب البطاقات إلى رزمة المهملات.
- حاول سحب وإفلات البطاقات بين مجموعات اللعب (Tableau Piles) و
مجموعات الأساس (Foundation Piles) ورزمة المهملات.
- تحقق من تطبيق قواعد اللعبة: هل يمكنك وضع بطاقة حمراء على بطاقة سوداء ذات
 رتبة أعلى بواحد؟ هل يمكنك وضع آس فقط على رزمة الأساس الفارغة؟
- اختبر حالة الفوز.

نصائح لتصحيح الأخطاء (Debugging):

- استخدم System.out.println() أو Logger لطباعة قيم المتغيرات وحالة اللعبة في نقاط مختلفة.
- استخدم ميزة تصحيح الأخطاء (Debugger) في NetBeans بوضع نقاط
 توقف (Breakpoints) في الكود لفهم تدفق التنفيذ وقيم المتغيرات خطوة بخطوة.
- تحقق من مسارات الصور في CardImages.java. الأخطاء في المسارات هي سبب شائع لعدم ظهور الصور.
- تأكد من أن الأبعاد والأوفست (Offsets) المستخدمة في GamePanel تتناسب مع حجم صور البطاقات لديك.

 الخلاصة

لقد قمت ببرمجة لعبة Solitaire وواجهتها الرسومية الأساسية باستخدام Java وNetBeans.
 لقد غطى هذا المقال جميع الخطوات الأساسية لتطوير لعبة Solitaire، من 
تصميم الفئات الأساسية ومنطق اللعبة إلى بناء الواجهة الرسومية باستخدام Swing ومعالجة تفاعلات المستخدم.
هذا المشروع يمثل نقطة انطلاق رائعة لـتطوير الألعاب بـ Java. 
يمكنك الآن التفكير في الميزات المستقبلية لتحسين لعبتك وجعلها أكثر احترافية،
باستخدام المفاهيم التي تعلمتها هنا، أصبحت لديك الآن الأسس اللازمة لمواصلة استكشاف
 عالم برمجة الألعاب باستخدام Java وتطوير ألعاب أكثر تعقيدًا.
 لا تتردد في مشاركة مشروعك والتعلم من مجتمع المطورين!


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