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
structwith 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
mallocand 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 inpoker.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:
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:
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:
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:
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 formalloc/free.) - Your code must compile with no warnings under
-Wall -Wextra -Werror. - One
freefor everymalloc. No leaks, no use-after-free, no double-free. This is part of your grade and is checked withvalgrind. - Do not change
poker.h,main.c, or theMakefile. You submit onlypoker.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).