Skip to content

HW3 - Poker Hand Evaluator

Due Date: July 22nd 2026, 05:00 PM

In this assignment you'll build a small poker engine: a deck of cards you can shuffle and deal, and the logic to evaluate and compare five-card hands. This is the first homework built around the ideas from the last two lectures:

  • structs (Lecture 7): model a card as a struct with a rank and a suit, and work with arrays of cards,
  • pointers to structs (Lecture 8): a deck that lives on the heap and that you read and modify through a pointer with the -> operator, and
  • heap ownership (Lecture 6): the deck allocates its cards with malloc and frees them exactly once.

You'll put these together into an evaluator that names any five-card hand (pair, flush, full house, and so on) and decides which of two hands wins. A pre-written main.c then turns your library into a working "showdown": it deals two hands from a shuffled deck and announces the winner.

  • Starter code: hw3/ - implement your code in poker.c.
  • Language standard / flags: clang -Wall -Wextra -Werror -std=c17
  • Submission: see Submitting below.

Getting Started

You already created your homework repository (homework-GITHUB_USERNAME in the uchicago-cmsc14300-sum-2026 organization) for HW1, so there is no new setup. Pull the HW3 starter files from the upstream repository:

$ cd cmsc14300-sum-2026/homework-GITHUB_USERNAME
$ git pull upstream main

This adds an hw3/ directory alongside your earlier work. If git pull upstream main reports a merge or a conflict, do not force anything - post on Ed and we will help.


Background

A card is a struct

Recall from Lecture 7 that a struct groups a few related values under one type. A playing card is a natural fit: it has a rank and a suit. The rank and suit information can be encoded as enums, which gives us the added benefit of type safety and the ability to order the rank.

The poker.h header gives you the two enums and the struct definition:

typedef enum {
    RANK_TWO = 2, RANK_THREE, ... , RANK_KING, RANK_ACE   /* values 2 .. 14 */
} rank_t;

typedef enum { SUIT_CLUBS, SUIT_DIAMONDS, SUIT_HEARTS, SUIT_SPADES } suit_t;

typedef struct {
    rank_t rank;
    suit_t suit;
} card_t;

The ranks are numbered so that a larger enum value is a higher card, and the ace is high (14). You can compare two ranks with < and >, and you can use a rank or suit as an array index because an enum is just a named integer.

A deck that owns its cards on the heap

A deck_t is a struct that owns a heap array of cards, exactly the pattern from Lecture 8 (a struct with a pointer member that points at memory the struct is responsible for):

typedef struct {
    card_t *cards;    /* heap-owned array of cards */
    int size;         /* cards remaining in the deck */
    int capacity;     /* total slots allocated (52) */
} deck_t;

deck_create allocates both the struct and its 52-card array with malloc and returns a deck_t *. Everywhere else you reach the members through that pointer with -> (for example d->size, d->cards[i]). deck_free gives both blocks back with free. Dealing a card removes it from the top of the deck (the slot at index size - 1) and decreases size.

Shuffling with a fixed generator

To shuffle we use the Fisher-Yates algorithm: This algorithm shuffles the elements of an array, with equal probability for each permutation.

The idea is to traverse the array from the last position down to the second, and for each position i pick a random earlier-or-equal (i.e. 0 to i) position j and swap. You may read about the algorithm on Wikipedia.

So that everyone's shuffle (and the demo) comes out identically for a given seed, you must use this exact pseudo-random generator, which generates a random number r in the range [0, 32767]. Keep a 32-bit unsigned state that starts at the seed; each time you need a number, first advance the state and then read its middle bits:

//Initial Seed:
seed = 42u;  /* or whatever the user entered */
state = seed;  /* initialize the state */

//When you need a random number:
state = state * 1103515245u + 12345u;   /* advance */
unsigned int r = (state >> 16) & 0x7fffu;  /* r is in [0, 32767] */

The u in the constants indicates unsigned integers.

Walk i from size - 1 down to 1. For each i, draw one r as above, let j = r % (i + 1), and swap cards[i] with cards[j]. Drawing exactly one number per step, in this order, is what makes your deck match the grader's.

Ranking a hand

Every hand has five cards. These are the standard poker hand rankings; the List of poker hands Wikipedia article shows an example of each with pictures of the cards. One difference from the article is the absence of wild cards (i.e. the Joker).

From weakest to strongest, the nine categories are:

Category What it is
High Card none of the below
Pair two cards of one rank, such as two Aces, two Kings, two 10s etc.
Two Pair two cards of one rank, two of another, such as two Aces and two Kings
Three of a Kind three cards of one rank, such as three Aces, three Kings, three 10s etc.
Straight five consecutive ranks, such as 10-J-Q-K-A, or A-2-3-4-5
Flush five cards of one suit, such as five Hearts, five Spades, etc.
Full House three of one rank and two of another, such as three Aces and two Kings
Four of a Kind four cards of one rank, such as four Aces, four Kings, four 10s etc.
Straight Flush a straight that is also a flush, such as 10-J-Q-K-A of Hearts

The hand_rank_t enum lists these in this order, so a larger category value always beats a smaller one.

Straights. Because the ace is high, the highest straight is 10-J-Q-K-A. Fun fact: If the straight is all of the same suit (Straight Flush), and is the highest straight, then that's a Royal Flush (e.g., 10-J-Q-K-A of Hearts).

The one special case is the wheel, A-2-3-4-5, which counts as a straight whose high card is the five (so it is the lowest straight, beaten by 2-3-4-5-6). The ace is high everywhere else, so K-A-2-3-4 is not a straight.

Breaking ties within a category. Suits never decide a poker hand, so two hands can genuinely tie. When two hands share a category, the usual poker rules break the tie (higher pair, then kickers (the remaining cards)), high to low, and so on); the exact order is documented in poker.h.


Homework 3 Tasks

Implement every function in starter/poker.c. The exact prototype and contract for each is in starter/poker.h. You may add your own static helper functions (a swap and a hand-scoring helper are natural ones).

Part 1 - Cards and the deck (25 pts)

Function Does
deck_t *deck_create(void) malloc a deck and its 52-card array in the specified order as described in poker.h; return the pointer (caller owns it), or NULL on allocation failure.
void deck_free(deck_t *d) Free the cards array and the deck (free(NULL) is fine).
card_t deck_deal(deck_t *d) Remove and return the top card (index size - 1) and decrease size.
void card_to_string(card_t c, char *buf) Write a card's short text form (for example "AS", "10H", "2C") into buf. You may use string.h functions.

Part 2 - Shuffle and sort (20 pts)

Function Does
void deck_shuffle(deck_t *d, unsigned int seed) Shuffle the deck in place with Fisher-Yates, using the exact generator above.
void hand_sort(card_t *hand, int n) Sort n cards in place, descending by rank, breaking ties by higher suit. You may implement any sorting algorithm, and since the deck
is 52 cards, the efficiency of the sort is not as critical.

Part 3 - Evaluate and compare (45 pts)

Function Does
hand_rank_t hand_evaluate(const card_t *hand) Return the best category the five cards form.
int hand_compare(const card_t *a, const card_t *b) Return > 0 if a wins, < 0 if b wins, 0 on a tie, using the standard tie-breakers.

Hints for hand_evaluate and hand_compare.

This is the hardest part of the assignment, so approach it in layers rather than all at once.

For hand_evaluate, two questions unlock every category: are all five suits the same (a flush), and are the five ranks consecutive (a straight)? Answer those two, then handle the rest with a count of how many cards you have of each rank. That same rank-count tells the pairs, three-of-a-kinds, and four-of-a-kinds apart: for example a four and a one is four of a kind, a three and a two is a full house, and two twos and a one is two pair. Check the categories from strongest to weakest and return the first that matches, and remember the wheel (A-2-3-4-5) counts as a straight.

For hand_compare, start with the easy half: call hand_evaluate on each hand first, and if the categories differ you already have your answer (the larger category wins) with no tie-breaking needed.

That leaves only the case where both hands are the same category. Rather than writing a separate tie-breaker for each of the nine categories, look at the list of tie-breakers in poker.h and notice they all have the same shape: you compare a sequence of ranks from most important to least, and the first difference decides it.

The question is only what order to line the ranks up in. Two cards of a rank should always be considered before a lone kicker, and three before two, regardless of the actual rank values, so a natural idea is to order each hand's ranks first by how many times the rank appears and then by the rank itself, and then walk the two ordered lists together until they differ. Build a small count of how many cards you have of each rank (an array indexed by rank, like the tables from Lecture 4, works well) and let that drive the ordering.

Finally, do not forget the wheel: A-2-3-4-5 must compare as five-high, not ace-high, so handle that one straight specially before you rely on the ace being the top card. Test the tie cases from the list below, and hands that share a category but split on a kicker, before you trust it.

Once these work, main.c becomes a working showdown:

$ ./app
Seed? 42
Player 1: KS QS 8H 8D 6H  ->  Pair
Player 2: AC 9H 4D 3S 3C  ->  Pair
Player 1 wins!

Building and testing

cd hw3/starter
make          # build the app
./app         # run the Poker Showdown

make test     # build & run the public Criterion test suite
make clean

Trying one function at a time

Running the whole app only tells you whether the entire pipeline works. While you are still building things up, it is much easier to call a single function by hand and print what it returns. For that, create your own scratch driver named try.c in hw3/starter/ with its own main() that calls whichever poker.c functions you want to exercise, then build and run it with:

make try      # compiles try.c against poker.c into ./try
./try

A minimal try.c might look like:

#include <stdio.h>
#include "poker.h"

int main(void) {
    deck_t *d = deck_create();
    deck_shuffle(d, 42);

    card_t c = deck_deal(d);
    char buf[4];
    card_to_string(c, buf);
    printf("top card = %s\n", buf);

    deck_free(d);
    return 0;
}

Change main() as you go to probe hand_sort, hand_evaluate, and hand_compare. try.c is just for you: you do not submit it, and the grader never builds it, so make try fails harmlessly until you create the file. The make clean rule removes the try executable along with the others.

The public tests in test_poker.c are a subset of what we grade with - passing them is necessary but not sufficient. Write your own tests and try edge cases: the wheel A-2-3-4-5, the ace-high straight 10-J-Q-K-A, K-A-2-3-4 (not a straight), two hands that tie, and a pair that is decided by its kicker.

Because you are managing memory by hand, check for leaks and invalid accesses:

valgrind ./app
valgrind ./test_poker

A correct solution reports no leaks and no errors. valgrind currently only works on Linux and Windows Subsystem for Linux (WSL). If you are on a Mac, you can use the macgrind option, or run your code on the CS cluster (Linux) to check for leaks.


Rules & academic integrity

  • Do not use qsort. Write the ordering logic yourself. (<stdlib.h> is fine and needed for malloc/free.)
  • Your code must compile with no warnings under -Wall -Wextra -Werror.
  • One free for every malloc. No leaks, no use-after-free, no double-free. This is part of your grade and is checked with valgrind.
  • Do not change poker.h, main.c, or the Makefile. You submit only poker.c, and it must build against the unmodified reference files.
  • You can individually test your code with try.c (see above), but the grader will not see it.
  • Generative AI policy (from the syllabus): do not use an LLM to write or fix your CS143 code, and do not paste course materials or your code into one. The work you submit must be your own. Cite any outside sources you consult (even small ones) in a code comment. If you find yourself stuck, reach out to the instructor on Ed or in person to get unstuck - do not spend more than an hour fighting a problem without asking for help.

Grading

Component Points
Part 1 - cards and the deck 25
Part 2 - shuffle and sort 20
Part 3 - evaluate and compare 45
Manual Grading 10
Total 100

The manual grading portion is for code style, comments, and memory safety (no leaks under valgrind). Please see the UChicago CS Style Guide for C for additional details.

Submitting

Submit poker.c (the only file you change) to Gradescope under "HW3". Make sure it builds against the unmodified poker.h, main.c, and Makefile. Remember the late policy from the syllabus (3 late days total for the quarter).