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 2: Hand evaluation and game logic

In Part 1, we built a single Card that holds information about itself and knows how to draw and animate itself. Now, we’ll use this Card class as a building block to implement the game logic of Plaisante. We’ll accomplish this across two classes:

Neither class performs any drawing, which will allow us to test both of them using our usual testCode() approach without having to open a Processing window.

1. Poker hands

Before we write any code, we’ll walk through the poker hands that Plaisante’s game logic is built on. If you’ve never played poker or are unfamiliar with poker hands, no worries! We’ll cover everything you need to know in this section.

A poker hand is a name we give to a group of cards based on the patterns of their values and suits. Plaisante uses eight hands. Here they are, ordered from best to worst:

HandExampleDefinition
Four of a Kind8-8-8-8-3one value appears four times
Full House4-4-4-2-2three of one value and two of another
Flush2-6-9-J-A, all heartsfive cards all of the same suit
Straight5-6-7-8-9five different values in a row
Three of a Kind7-7-7-2-9one value appears exactly three times
Two PairJ-J-4-4-8two different values each appear twice
PairK-K-3-7-9one value appears exactly twice
High CardA-9-7-4-2none of the patterns above

Some things to note about this table. First, the order of the cards doesn’t matter: 9-8-7-6-5 is the same straight as 5-6-7-8-9. Second, a Straight, a Full House, and a Flush all need exactly five cards because of their definition. The other hands can be played with fewer than five. Finally, a hand can match more than one row. Five cards in a row that are also all one suit count as a Flush in Plaisante, because a Flush is the better hand.

In Plaisante you may play anywhere from one to five cards at a time, so a two-card selection of K-K is a perfectly legal Pair.

2. Evaluating a hand

Let’s now build the Hand class that implements this logic. A Hand object holds a list of up to 5 cards, and works out which poker hand they form and what that hand scores.

Hand stores its cards in a List<Card>, in an instance variable named cards. Recall from lecture that a List keeps its elements in order and knows its own length. The two methods we’ll need to read it are:

  • cards.size(): how many cards are in the hand.

  • cards.get(int index): the Card sitting at that index, counting from 0.

With this in mind, we’ll write some helper methods that will aid in hand evaluation before writing the checks that recognize each hand.

[Task] Drawing the counts array

Let’s start with a helper method that counts the occurrences of each value in the hand, called countValues(). Most of the hands in the table are really a question about repeats: does some value show up twice, three times, four times?

One initial approach would be to loop through the hand once per value, checking if the value appears twice, three times, four times, etc. However, this could get pretty inefficient as our game logic needs to check for all of these repeats among all possible values.

Instead, you’ll write countValues() to pre-compute the counts for the cards in a single loop through the hand. Here, we use the array index to represent the value of the card, and then the cell at that index to represent the count of the card. For example, if the hand holds a pair of sevens, then index 7 holds the number 2.

Card values run from 2 (a two) up to 14 (an ace), so the array needs to be length 15, since we need an index for every value we might see (thus index 14 needs to exist). Note that indices 0 and 1 never get used, because no card is worth 0 or 1. This unused space may feel a little wasteful, but this tradeoff is worth it to be able to use a card’s value directly as an index.

Counting the values in K-K-3-7-9. Each card adds one to the cell matching its value, so the two kings both land in the cell at index 13. If a card does not appear in the hand, the cell remains at 0.

Counting the values in K-K-3-7-9. Each card adds one to the cell matching its value, so the two kings both land in the cell at index 13. If a card does not appear in the hand, the cell remains at 0.

Before writing the method, let’s trace an example by hand.

[Task] countValues() method

Now that you’ve drawn it out, let’s implement it in code for the full deck of cards:

  1. Create a new array of 15 integers.

  2. Walk every card in the cards list, adding 1 to the cell matching that card’s value.

  3. Return the array.

[Task] Code-a-little, test-a-little

We’ll put into practice the code-a-little, test-a-little approach once more for this assignment, as there are many small methods you’ll be writing.

As before, you’ll write your checks inside the static testCode() method at the bottom of Hand.java, which main() calls for you. Recall the three steps most tests follow:

  • Set up a small, predictable input.

  • Run your code on the input by calling the method you’re testing.

  • Check for correctness by comparing the actual output to the expected output.

We’ve provided the setup for the K-K-3-7-9 hand from the figure above, along with a check for countValues() to get you started:

List<Card> pairCards = new ArrayList<>();
pairCards.add(new Card("K", "hearts", 0, 0));
pairCards.add(new Card("K", "diamonds", 0, 0));
pairCards.add(new Card("3", "clubs", 0, 0));
pairCards.add(new Card("7", "spades", 0, 0));
pairCards.add(new Card("9", "hearts", 0, 0));
Hand testHand = new Hand(pairCards);

int[] counts = testHand.countValues();
if (counts.length != 15) {
    System.out.println("Failed test for countValues() of K-K-3-7-9: should have 15 indices but got "
            + counts.length);
}

Add a few more tests for countValues() that check individual cells: one cell that should hold a count, and one that should hold 0.

Note that because testCode() lives inside the Hand class, it’s allowed to call countValues() even though it’s private.

[Task] Count-based has...() hands

hasPair(), hasThreeOfAKind(), hasFourOfAKind() hands

Now that we have countValues(), we can write many of the checks for the hands in the hand table more easily. All of the has...() methods will return true if the given hand is detected, and false otherwise.

For example, hasPair() returns true when some value appears exactly twice. The approach is:

  1. Get the counts array by calling countValues().

  2. Walk through the counts array, looking at each index.

  3. If you find a cell holding exactly 2, the answer is true.

  4. If you get all the way through without finding one, the answer is false.

Note that step 3 says exactly two, not two or more. This is intentional, as it makes subsequent checks easier to write.

hasThreeOfAKind() and hasFourOfAKind() follow the same structure as hasPair(), with a different count to check.

hasTwoPair() and hasFullHouse() methods

Next, let’s write two more checks that build on the three you just wrote.

hasTwoPair() is a variation on the structure above. Rather than stopping at the first value that appears twice, we need to know how many values appear exactly twice.

hasFullHouse() can be written using your previous methods, as a full house is three of one value and two of another.

[Task] Test the count-based hands

Now that you’ve added methods for the count-based hands, it’s time to test them. Add tests to testCode() to verify your implementation, following the best practices of testing we’ve discussed in lab. Like for countValues(), we’ve provided some template code building a hand in testCode() to get you started. Feel free to copy and modify it as you see fit!

[Task] hasFlush() method

The last two has...() methods walk the cards themselves rather than reading only the counts array.

A flush is five cards that all share one suit. The overall approach is to start by assuming it is a flush, then let any card that disagrees rule it out. We begin by saving the suit of the first card in a variable, and then we’ll check each card against it. If we ever find a card that doesn’t match the first card’s suit, we can immediately return false. If we get through all five cards without finding any that don’t match, we can return true.

[Task] hasStraight() method

Finally, we have the trickiest check in the class: hasStraight(). Let’s work out the approach before writing any code.

A straight is five different values in a row, like 5-6-7-8-9, in any order. A useful observation is that in any five values in a row, the highest is always exactly 4 above the lowest. In 5-6-7-8-9, the highest is 9 and the lowest is 5, and 9−5=49 - 5 = 4. In 9-10-J-Q-K, the highest is 13 (for the king) and the lowest is 9, and 13−9=413 - 9 = 4. That gives us something we can measure, provided we know what the highest and lowest values are. That test isn’t enough on its own, though.

To find the highest and lowest values in a hand, we’ve provided two helper methods in the starter file:

  • lowestValue() returns the value of the lowest card in the hand.

  • highestValue() returns the value of the highest card in the hand.

Putting all of this together, hasStraight() has three checks to make:

  • The hand has exactly five cards.

  • The difference between highestValue() and lowestValue() is exactly 4.

  • No value appears more than once.

Test hasFlush() and hasStraight()

We’ve written the tests for hasFlush() and hasStraight() for you. Scroll to the end of testCode() in Hand.java and read through the two hands they check:

  • 2-3-4-5-6, all hearts, which is both a flush and a straight.

  • 2-3-4-5, all hearts, which is neither because it only has four cards.

Run Hand.java by clicking the play button. If no failure messages are printed for these hands, your two methods are working.

We’re now finished with the Hand class!

(Optional) How a hand is scored

With the has...() methods written, Hand can recognize each of the eight hands. We’ve provided the methods that compute a hand’s score in the starter file, so there’s no code for you to write in this section. Let’s walk through how they work.

A hand’s score comes from two places:

  • its total value, which is the sum of the values of the cards played

  • its multiplier, which comes from the hand

Put together:

score=total value×multiplier\text{score} = \text{total value} \times \text{multiplier}

Each hand has a different multiplier. The better the hand, the larger the multiplier:

HandMultiplier
Four of a Kind12
Full House9
Flush8
Straight7
Three of a Kind6
Two Pair3
Pair2
High Card1

For example, K-K-3-7-9 in mixed suits is a Pair. Each king is worth 13, so its total value is 13+13+3+7+9=4513 + 13 + 3 + 7 + 9 = 45, and a Pair multiplies by 2 for a score of 45×2=9045 \times 2 = 90.

To look up the multiplier, we first need to know which hand the cards form. The evaluate() method determines what the hand is by checking each of the possible hands from best to worst and returning the first one that is found:

public String evaluate() {
    if (hasFourOfAKind()) {
        return FOUR_KIND;
    } else if (hasFullHouse()) {
        return FULL_HOUSE;
    } else if (hasFlush()) {
        return FLUSH;
    } else if (hasStraight()) {
        return STRAIGHT;
    } else if (hasThreeOfAKind()) {
        return THREE_KIND;
    } else if (hasTwoPair()) {
        return TWO_PAIR;
    } else if (hasPair()) {
        return PAIR;
    } else {
        return HIGH_CARD;
    }
}

The order of these conditionals carries the logic here, because more than one check can be true of the same five cards and we want the best hand to be returned.

Three other provided methods complete the scoring logic:

  • totalValue() returns the sum of the values of the cards in the hand.

  • baseMult() calls evaluate() to get the hand, then returns its multiplier from the table.

  • score() returns totalValue() multiplied by baseMult().

3. Organizing cards in a Tableau

The Tableau[1] class is responsible for organizing the cards in the game. It keeps track of cards held by the player, cards selected by the player, and cards played:

  • heldCards: the row of up to 8 cards the player can choose from.

  • selected: the cards the player has clicked, which is always a subset of the held cards.

  • playArea: the cards the player has committed to playing.

The three lists a Tableau keeps track of. In the top panel, the two clicked cards are in selected and still in heldCards, which is why they stay in the row while they’re highlighted. In the bottom panel, pressing Play has moved them into playArea and emptied selected.

The three lists a Tableau keeps track of. In the top panel, the two clicked cards are in selected and still in heldCards, which is why they stay in the row while they’re highlighted. In the bottom panel, pressing Play has moved them into playArea and emptied selected.

Over a round, a card might travel through all three lists. It’s dealt from the deck into heldCards, clicked into selected, moved into playArea when the player presses Play, and cleared out once the round moves on. We’ve provided some completed methods in Tableau.java, and feel free to read their javadoc comments if you want to know more about them.

Your job is to write the two methods that move cards between the selected, heldCards, and playArea lists. For that, we’ll use some of the List methods we’ve discussed in class and lab. Here’s a reference of the ones we might need:

  • list.add(Card card): adds a card to the end of the list.

  • list.remove(Card card): removes that card from the list.

  • list.remove(int index): removes the card at that index, and returns it.

  • list.contains(Card card): returns true if that card is in the list.

  • list.clear(): empties the list completely.

Note that remove() behaves differently depending on what parameter you pass it. Given a Card, it removes that card; given an int, it removes whatever is at that index.

[Task] toggleSelection() method

We begin with the toggleSelection() method. This method is called with the card the player just clicked, and its job is to select that card, or deselect it if it was already selected.

There are three cases that this method needs to handle:

  1. If the card is already in the selected list, remove it. Deselecting is always allowed.

  2. Otherwise, if the card is not in the selected list and there are fewer than MAX_SELECTED cards already selected, add it to the selected list.

  3. If the card isn’t selected and the selection is already full, do nothing.

MAX_SELECTED is a constant provided at the top of the class.

[Task] moveSelectedToPlayArea() method

Finally, the moveSelectedToPlayArea() method is called when the player presses Play. Every selected card leaves the held cards and moves to the play area, and afterwards nothing is selected any more. The implementation approach is to:

  1. Walk every card in the selected list. For each one, remove it from heldCards and add it to playArea.

  2. Once the loop has finished, empty the selected list.

Note that you don’t need to modify the coordinates of the cards in the lists, as the Plaisante class handles the animation.

Test your Tableau class

Unlike Hand, we’ve written all the tests for this class for you. Scroll to testCode() at the bottom of Tableau.java and read through what it does: it builds a small 10-card deck and deals 8 cards from it using the provided dealFrom() method. Then it selects five cards and tries to select a sixth, deselects two, and moves the remaining three to the play area, checking the state of the lists as it goes. This should closely match the manual checks you did in the preceding “Check your understanding” questions!

Run Tableau.java by clicking the play button. Refer to your “Check your understanding” questions to see what you should expect to see if any of the tests fail.

We’re now finished with the Tableau class!

(Optional) How Plaisante works

You’ll notice that right now we have three classes that form components of our game, but we’re still lacking something that puts everything together into a full game. That’s where the Plaisante class from the starter code comes into play: it’s the class that coordinates between your Card, Hand, and Tableau classes to manage the game state. Exactly how it does that determines the overall game logic, just like how we can pair different sets of rules with the same physical deck of cards to get different games (e.g., solitaire, speed, poker, spoons). We could thus create classes that use our same three components in different ways to create other video games.

The other crucial task that Plaisante.java does is build the user interface (UI). This refers to how we collect and evaluate user input (such as translating a click into a method call), as well as what the user sees on screen. Building a UI from scratch can be complex and time-consuming, particularly if you want to fine-tune the visual aesthetics of your game. Game design can be a whole course in itself (we encourage you to check out Professor Murphy McCauley’s Game Programming courses!), so to streamline the process and keep our focus on the data structures and game logic, we’ve provided the Plaisante.java class for you.

We’d like to underscore that the classes you worked to create are the ones that matter the most. Without them, there would be no game! Going back to the initial assignment description, imagine working on a team of game developers, where your job was to build the game logic and Card classes, while your teammates were responsible for the UI. In fact, you could use these same components you built to make a more exciting version of Plaisante, or even entirely new games. If you want to understand how we could approach that, we highly encourage you to take a peek at the provided Plaisante.java class, as you can see how your code is used as part of larger game logic:

When...Plaisante calls...
A player clicks somewhere in the windowtableau.cardAt() to find which card was clicked, then applies tableau.toggleSelection() to it
A player presses Playtableau.moveSelectedToPlayArea(), then builds a Hand from the play area and asks it to evaluate() and score()
The play area clearstableau.dealFrom(deck), then positions each card it gets back so the new cards animate out of the deck

Note that Plaisante only ever handles the game state and the UI. It asks Tableau to do the moving and asks Hand for the score. Each class encapsulates its own data and functionality, which is what allowed us to test both Hand and Tableau using our usual testCode() approach without having to open a Processing window.

4. Demo your Plaisante game

With Card, Hand, and Tableau all working, you can play the completed game by running Plaisante.java.

You have 4 hands to play and 4 discards to spend. Play through a full round and check that:

  • Eight cards animate out of the deck when the round starts.

  • Clicking a card highlights it, and clicking it again removes the highlight. A sixth card can’t be selected until you deselect one.

  • Pressing Play moves your selection into the play area, and the hand’s name and score appear.

  • The played cards clear away, and new cards animate out of the deck to bring you back up to eight.

  • Pressing Discard clears your selection the same way, but without scoring it.

  • After 4 played hands, the round ends and your total is displayed.

When your round plays through cleanly, you’ve finished your part of Plaisante. Congratulations, and enjoy the game!

5. Reflection

Finally, answer the following reflection questions in a file named README.md and submit it to Gradescope:

  1. Approximately how much time did you spend on the final PA 2 submission?

  2. What were the most challenging portion(s) of the assignment?

  3. What is one thing you have a better understanding of after completing this assignment and going through the class content?

  4. Do you have any follow-up questions about concepts that you’d like to explore further?

  5. Indicate the number of late days (if any) you are using for this assignment.

Part 2 task checklist

Going further

If you want to keep going after finishing the assignment, here are a few directions to explore. None of these are required!

  • Let aces be low as well as high, so that A-2-3-4-5 counts as a straight the way it does in real poker. Right now hasStraight() leans on an ace being worth 14, so the span between the ends comes out as 12 rather than 4. You’d need a second case for this one specific run, placed so the rest of the method stays readable.

  • Add a straight flush, five cards that are both in a row and all of one suit, as a ninth hand. You’d need a new name constant, a new conditional in evaluate(), and a multiplier. Think back to what the reordering check showed about conditionals that are true at the same time to decide where the new one goes.

Footnotes
  1. “Tableau” is a commonly-used board game word for the cards laid face-up in front of you. You might even hear about “tableau building” as a game mechanic.