Theoretical Solitaire Design Questions :)

Hi guys!

In my spare time I have been designing the classes for a Solitaire game I am hoping to program on the weekend. However I’m stuck on a particular problem.

Does anyone have any suggestions as to how to implement the ‘drag-ability’, of the solitaire cards, more how to distinguish between a card on the top of the stack, versus a card lower down the stack. Basically I’m stuck on how to deal with each card individually according to their position within the solitaire stack.

My initial solution was to have the Card class extend JComponent so I could determine when the mouse was within each component, or ‘card’, then handle them appropriately according to the mouse movement.

There must be a simpler way? If anyone has any suggestions I would be very very appreciative :slight_smile:

I think you could make a variable that is like numberOnStack where If 0 then if mouse touching reveal and a function that goes when dragged all the other cards have their variable decreased…

I’d hold each card in a stack in an array. And for each card within the stack, calculate its ‘visible/clickable extent’, i.e. the top of the card poking out from within the stack, which could be stored as a Rectangle (this may be constant, or adjust dependant on the number of cards in the stack).

When you click on a stack, check which card’s visible extent contains the click, by iterating through the cards in the stack array.

You could then use its array index to handle all cards above it. I.e. if a stack contains 13 cards, where stack.cards[0] is the card at the bottom of the stack, and stack.cards[12] is the topmost card, if your click detection determined you’ve clicked on stack.cards[8], then you apply the drag event to all cards from stack.cards[8] through to stack.cards[12].

Yeah man, I was thinking that too (the clickable top rectangle), it seems like the most logical solution. You could handle the clickable areas sort of like tabs within the stack group, and like you said, they would lead the movement of the remainder of the stack.

I think I might go with this solution unless anyone else has a more efficient approach :slight_smile: Plan to post the source code here when it’s done (hopefully over the weekend).

Yay programming! ;D

Not sure why my drag controller class is working :frowning: If someone could help me out that would be AWESOME!!! ;D

(made a test project to play around with the drag controller class)

EDIT: Sorry for the double post!

MAIN SOLITAIRE CLASS:

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.ArrayList;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Solitaire extends JPanel {

	private static final long serialVersionUID = 1L;
	
	private final static int GAME_WIDTH = 900;
	private final static int GAME_HEIGHT = GAME_WIDTH / 16 * 9;
	
	private JFrame frame;
	private Card card;
	
	private ArrayList<Card> cards = new ArrayList<Card>();
	
	public Solitaire() {
		this.setPreferredSize(new Dimension(GAME_WIDTH, GAME_HEIGHT));
		this.setFocusable(true);
		this.setVisible(true);
		
		frame = new JFrame();
		
		card = new Card();
	}
	
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		Graphics2D g2d = (Graphics2D)g;
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		
		card.draw(g2d);
	}
	
	public static void main(String[] args) {
		Solitaire game = new Solitaire();

		game.frame.setResizable(false);
		game.frame.add(game);
		game.frame.pack();
		game.frame.setLocationRelativeTo(null);
		game.frame.setVisible(true);
		game.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		
	}

}

CARD CLASS:

package com.eternalgames.javasolitaire;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;

import javax.swing.JComponent;

public class Card extends JComponent{
	
	private static final long serialVersionUID = 1L;

	private DragController dragController;
	
	private final int CARD_WIDTH = 100;
	private final int CARD_HEIGHT = 150;
	
	public int x = 0;
	public int y = 0;
	
	public Card() {
		dragController = new DragController(this);
	}
	
	public void draw(Graphics2D g2d) {
		g2d.setColor(Color.red);
		g2d.fillRect(x, y, CARD_WIDTH, CARD_HEIGHT);
	}

	public void setCardPos(int x, int y) {
		this.x = x;
		this.y = y;
	}
	
	@Override
	public Dimension getSize() {
		return new Dimension(CARD_WIDTH, CARD_HEIGHT);
	}
}

DRAG CONTROLLER CLASS:

package com.eternalgames.javasolitaire;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

import javax.swing.event.MouseInputAdapter;

public class DragController extends MouseInputAdapter {
	
	private Card card;
	
	private Point offset = new Point();
	private boolean dragging = false;
	
	public DragController(Card card) {
		this.card = card;
		
		card.addMouseListener(this);
		card.addMouseMotionListener(this);
	}

	public void mousePressed(MouseEvent e) {
		Point p = e.getPoint();
		Rectangle r = new Rectangle(card.getSize());
		
		if (r.contains(p)) {
			offset.x = p.x - r.x;
			offset.y = p.y - r.y;
			dragging = true;
		}
	}
	
	public void mouseReleased(MouseEvent e) {
		dragging = false;
	}
	
	public void mouseDragged(MouseEvent e) {
		if (dragging) {
			int x = e.getX() - offset.x;
			int y = e.getY() - offset.y;
			card.setCardPos(x, y);
		}
	}
}

Howdy,

It’s not working because you treat the card as a JComponent, but you never actually add the card to a parent component. Hence even though you are drawing it, its not actually ‘there’ for click detection etc.

I’d also suggest that you don’t do all that initialization within the main() method. That can all be moved to the Solitaire constructor.

Perhaps others would disagree with me, but I’m not too keen on your design here; using JComponents, having individual ‘drag handlers’ for each card etc. I’d use a single instance of a MouseInputListener, and handle everything from there.

Here’s something I ripped up quickly, which is roughly how I would do the same thing:

Main Solitaire class:

package solitaire;

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import javax.swing.event.*;
import java.util.*;

public class Solitaire implements MouseInputListener {
    
    ArrayList<Card> cards = new ArrayList<>();
    
    JFrame frame;
    SolitairePanel panel;
    
    int screenWidth = 800;
    int screenHeight = 600;
    
    //variables for card selection
    boolean cardSelected;
    Card selectedCard;
    Point lastPosition = new Point();
    
    Solitaire() {
       
        //Setting up the display
        frame = new JFrame();
        panel = new SolitairePanel(screenWidth, screenHeight, this);
        panel.addMouseListener(this);
        panel.addMouseMotionListener(this);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.add(panel);
        frame.pack();
        frame.setVisible(true);
        
        //Adding some cards
        cards.add(new Card(50,50));
        cards.add(new Card(200, 200));
        
    }
    
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Solitaire solitaire = new Solitaire();
        solitaire.go();
    }
    
    public void go(){
        
        //Bad loop just for demonstration.
        while(true){
            frame.repaint();
            try {
                Thread.sleep(16);
            } catch (Exception ex) {
                
            }
        }
    }

    @Override
    public void mousePressed(MouseEvent me) {
        
        for(int x = 0; x < cards.size(); x++) {
            if(cards.get(x).bounds.contains(me.getPoint())) {
                this.cardSelected = true;
                selectedCard = cards.get(x);
                lastPosition.setLocation(me.getPoint());
                break;
            }
        }    
    }

    @Override
    public void mouseDragged(MouseEvent me) {

        if(cardSelected){
            selectedCard.move((int)(me.getX() - lastPosition.getX()), (int)(me.getY() - lastPosition.getY()));
        }
        
        lastPosition.setLocation(me.getPoint());
    }
    
     @Override
    public void mouseReleased(MouseEvent me) {
        
         //NOTE: you would probably check if the card move is valid here. If so, update game, stacks, bounds etc. If not, return card to original position.
         if(cardSelected) {
            selectedCard.updateBounds();
            cardSelected = false;
         }       
    }   
    
     
     //Following methods aren't used.
     
    @Override
    public void mouseClicked(MouseEvent me) {}
    
    @Override
    public void mouseMoved(MouseEvent me) {}
    
    @Override
    public void mouseEntered(MouseEvent me) {}

    @Override
    public void mouseExited(MouseEvent me) {}
}

SolitairePanel - for drawing etc.:

package solitaire;

import java.awt.*;
import javax.swing.*;

public class SolitairePanel extends JPanel {
    
    Solitaire game;
    int width;
    int height;
    
    SolitairePanel(int width, int height, Solitaire game) {
        this.game = game;  
        this.width = width;
        this.height = height;
        this.setPreferredSize(new Dimension(width, height));
    }
    
    @Override
    public void paint(Graphics g) {

        g.setColor(Color.black);
        g.fillRect(0, 0, width, height);
        
        for(int x = 0; x < game.cards.size(); x++) {
            game.cards.get(x).render(g);
        }
        
    }
}

Card class:

package solitaire;

import java.awt.*;

public class Card {
    
    int width = 120;
    int height = 150;
    
    int xPos;
    int yPos;
    
    Rectangle bounds;
    
    Card(int xPos, int yPos) {
        this.xPos = xPos;
        this.yPos = yPos;
        bounds = new Rectangle(xPos, yPos, width, height);
    }
    
    //Move the card by setting the offset from its current position.
    public void move(int xOffset, int yOffset) {
        
        xPos += xOffset;
        yPos += yOffset;
        
    }
    
    //Update the bounds of the card, so that it can be selected.
    public void updateBounds() {
        bounds.setBounds(xPos, yPos, width, height);
    }
    
    public void render(Graphics g) {
        g.setColor(Color.red);
        g.fillRect(xPos, yPos, width, height);
        g.setColor(Color.blue);
        g.drawRect(xPos, yPos, width, height);
    } 
}

Hope this helps!

Hey Nerb, thank you for taking the time to write me that detailed response, you are a legend! I actually learned a few things by your post, one of those ‘click’ moments, which is awesome! ;D

No problemos! Good to hear. :slight_smile: