Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

PA 2 part 1: Drawing Card objects

Your first step in building Plaisante is a Card class. For the game, we will create one Card object for every card in a standard 52-card poker deck. Each Card object is responsible for:

We’ll implement these functionalities one at a time.

Setting up the Card class

Let’s begin by setting up the data the Card class holds through the instance variables and how new Card objects are created through the constructor. Open the starter code for the Card class, which shows the constants and the instance variables you will use in your implementation.

[Task] Complete the instance variables and constructor

Your instance variables hold all of the data about a card. You’ll declare the following four, following the TODOs in the starter file. Make sure to declare them as private.

The other four are already declared for you in the starter file:

Next, complete the constructor. The Card() constructor takes a rank, a suit, and a starting position, setting up all of the instance variables as follows:

  1. Store suit and rank directly from the parameters.

  2. Set x and y from the parameters too.

  3. Set targetX and targetY to the same starting position as x and y.

  4. Set value using the getCardValue(String rank) helper method, which is provided for you in the starter file.

  5. Set isRed to true when the suit is hearts or diamonds, and false otherwise.

[Task] Getters and setters

We declare instance variables private to ensure that nothing outside the Card class can modify or read the data each card represents, unless we purposely allow access by writing getter and setter methods. Getter methods return the value of an instance variable, and setter methods change the value of an instance variable. We’ve provided two getters in the starter file already:

public int getWidth() {
    return CARD_WIDTH;
}

public int getHeight() {
    return CARD_HEIGHT;
}

The starter file also has the rest of the getters and setters written as incomplete methods for you to fill in. For example, here is getX():

public float getX() {
    // TODO your code here
    return 0f;
}

The return 0f; line is a placeholder that lets the file compile, so replace it with your own return statement. Using the two provided getters as your model, complete getX(), getY(), getSuit(), getRank(), and getValue() so that each returns its instance variable. Then, complete the remaining three methods:

[Task] Test your Card class

As your programs grow in size and complexity, it becomes increasingly important to test your code early and often before you add more features. We refer to this as the “code-a-little, test-a-little” approach. Let’s take some time here to practice this approach by writing some tests for the methods you just implemented. As you did in PA 1, you’ll write your checks inside the static testCode() method provided at the bottom of the starter file, which main() calls for you. Recall the three steps most tests follow:

Like before, you can do this by writing if statements to compare the actual output to the expected output:

Card card = new Card("Q", "hearts", 0, 0);

if (!card.getRank().equals("Q")) {
    System.out.println("Failed test for getRank: expected Q but got " + card.getRank());
}

Write code that tests the following (you can use the “Check your understanding” example above as a guide):

  1. Card values. Build a face card (e.g., “Q”) and check that getValue() gives the correct value. Do the same for a number card like “7”.

  2. toString(). Build a card you know and check the label it produces. Make sure to compare String values with .equals(), not ==.

You can run your tests by clicking the play button for the Card.java file.

Drawing a Card object

Next, you’ll put together the visual elements of the card, which includes: 1) the card’s white background and 2) the rank and suit symbols.

Order of operations for Processing drawing

In PA 1, ToneMatrix extended PApplet, which meant the class was the Processing application itself. That’s why you could call fill() and rect() on their own, as they were already part of the application.

The Card class is different. A card isn’t a window, it’s an object that gets drawn into a Processing application window. So the Processing application gets passed to the card as a parameter named app, and every drawing call has to say which application it’s talking to. We provide one such PApplet application as CardMain.java.

// the app variable refers to the Processing application
app.fill(255);

To draw the card, we call the render(app) method, which is provided for you in the starter file:

public void render(PApplet app) {
    // Ensures that the text is centered on the card
    app.textAlign(PApplet.CENTER, PApplet.CENTER);

    drawBackground(app);
    drawRankAndSuit(app);
}

Notice the order of the two calls to drawBackground() and drawRankAndSuit(). The white card background has to go down first, or it would paint over the rank and the suit instead of sitting behind them. Your job is to implement these two methods.

Here are the Processing drawing methods you’ll need. You’ve seen fill(int rgb) and rect() before in PA 1, and the rest are new. Notice that all of these methods are called on the app parameter, which is the Processing application that the card is being drawn in:

[Task] Drawing exercise on card coordinates

A card’s x and y are its top-left corner. Everything printed on the card gets placed relative to that corner, as an offset to that top-left x-y coordinate. This allows the card to be animated across the screen without the drawing code changing at all, as the offset stays the same while the card’s position changes.

We provide constants that state the card is CARD_WIDTH (100) pixels wide and CARD_HEIGHT (140) pixels tall. The three things we print (the rank twice, and the suit symbol once) land at these offsets from the corner:

WhatText sizeOffset from the card’s (x,y) corner
rank, upper left24(x + 20, y + 20)
rank, lower right24(x + CARD_WIDTH - 20, y + CARD_HEIGHT - 20)
suit symbol, middle48(x + (CARD_WIDTH / 2), y + (CARD_HEIGHT / 2))

Note that because render() sets textAlign(CENTER, CENTER), when we call app.text(String words, float x, float y) our text will automatically be centered both horizontally and vertically around the coordinate we pass in. This is in contrast to methods like rect() that by default treat the coordinate passed in as the upper left corner of our shape. So when you draw this on paper, mark a dot at the offset and center the character on the dot. Use the figure below as a guide.

The anatomy of a card. The card’s own position is its top-left corner, and everything printed on it is placed as an offset from there. The dots mark where each piece of text is anchored, and because the text is center-aligned, each dot sits at the middle of its character.

The anatomy of a card. The card’s own position is its top-left corner, and everything printed on it is placed as an offset from there. The dots mark where each piece of text is anchored, and because the text is center-aligned, each dot sits at the middle of its character.

[Task] Complete drawBackground()

Now that we have the card’s visual elements drawn by hand, let’s translate them into code.

The drawBackground() method draws the blank card as a white rounded rectangle with a thin black outline. The starter file lists the steps as TODOs, in the order they need to happen:

private void drawBackground(PApplet app) {
    // TODO set the fill to white

    // TODO set the stroke to black

    // TODO set the stroke weight to 1

    // TODO draw the rectangle with the given x, y, width, height, and corner radius

}

Each TODO is one line of code. The values you’ll need are:

The first three TODOs are needed because of the Processing drawing order and style settings we discussed above: we set every style this method depends on at the beginning of the method. If we don’t, we risk carrying over unrelated style settings from the most recent drawing.

The only piece of syntax that’s new is the fifth argument to rect():

// a 60 by 40 rectangle at (x,y) positions at (10, 10), with corners rounded by 10 pixels
app.rect(10, 10, 60, 40, 10);

[Task] Complete drawRankAndSuit()

Next, we’ll implement the text printing on the card. Matching the drawn diagram, the rank is printed in two opposite corners with a text size of 24 and the suit symbol is printed in the middle with a text size of 48. All of the text is in the same color, red for hearts and diamonds and black for clubs and spades based on the card’s isRed boolean. As before, the starter file lists the steps as TODOs:

private void drawRankAndSuit(PApplet app) {
    // TODO set the fill color to be red or black depending on isRed

    // TODO set the text size to 24

    // TODO draw the rank in the upper left corner

    // TODO draw the rank in the lower right corner

    // TODO set the text size to 48

    // TODO draw the suit symbol in the middle

}

Visually testing your Card implementation

Once you’ve implemented the drawing methods, you can visually test them by running CardMain.java. Open the file and click the play button in VSCodium. If your implementation is working correctly, you should see an Ace of Spades rendered in the window, with a grid overlayed on top:

Animating a Card

Next, let’s implement the animation logic for our Card class.

[Task] update() method

Recall from PA 1 that a Processing application’s draw() method runs repeatedly, repainting the whole window each time. This repeated redrawing is what makes animation possible: nothing on screen actually moves, we redraw it somewhere slightly different, over and over, fast enough that our eyes read it as motion.

So a card animates by nudging itself a little closer to its target position each call to draw() through the update() method:

public void draw() {
    // update the card position to be closer to its target position
    card.update();
    // then draw the card
    card.render(this);
}

Specifically, with each call to update(), we want the card to move 10% of the gap between where it currently is (x,y) and where it’s going (targetX, targetY).

Processing provides functionality for this through the PApplet.lerp(start, stop, amount) method[2], which returns a float that is the interpolated value between start and stop based on the amount parameter. Specifically, the three parameters are:

Continually moving 10% of the remaining distance to the target position allows a card to “glide” smoothly to its position and settle, instead of moving at a flat speed:

A demo animation of the completed game dealing cards from the deck.

One way to picture this: a card at x=100x = 100 heading for x=200x = 200 has a gap of 100 pixels, and if we’re moving 10% of that gap it updates to x=100+0.1×100=110x = 100 + 0.1 \times 100 = 110. Then, on the next update the gap between x=110x = 110 and x=200x = 200 is 90 pixels, so x=110+0.1×90=119x = 110 + 0.1 \times 90 = 119. And so on: the next update lands at x=127.1x = 127.1. The gap shrinks every frame, so the steps shrink along with it. However, there’s a catch: closing 10% of the gap over and over never quite gets you exactly to the target number. The solution is to check once we’re within a pixel of the target, and “snap” the card to the exact target position:

Putting this together, update() has two parts:

  1. Set the card’s position to the lerp of x toward targetX, and the lerp of y toward targetY, both by 10%.

  2. If the card is now within 1 pixel of its target on both axes, set its (x,y) position to exactly (targetX, targetY).

To check if two values are close to each other, we can use the Math.abs() method. For example the below code checks if the difference between a and b is less than 1:

if (Math.abs(a - b) < 1) {
    System.out.println("a and b are within 1 pixel of each other");
}

Below update() in the starter file are two methods that are provided for you, which the game will use in Part 2:

Demo your implementation with CardMain.java

Once you’ve implemented update(), run the CardMain.java file again. In addition to rendering the card as before, clicking in the window will update the card’s target position, causing it to animate smoothly to be centered at that new position:

Also feel free to change the card’s rank and suit in the setup() method to check that different cards draw the way you expect.

When your tests pass and your card animates correctly, you’ve finished Part 1. Congratulations! In Part 2, we’ll build the hand evaluation logic that makes the game work.

Part 1 task checklist

The checkpoint autograder will grade your implementation of:

It will also provide feedback (but no points) on your implementation of:

Footnotes
  1. This is the RGB color model, the same one behind the greyscale numbers you used in PA 1. A single number like fill(200) is actually shorthand for an equal mix of all three e.g. fill(200, 200, 200). This equal mix comes out as some form of greyscale.

  2. “Lerp” is short for linear interpolation, which is the general idea of picking a value partway between two others. It shows up all over graphics and animation, usually as a way to make something move or fade smoothly.