Texas Hold’em with Cgreen

I’ve ben involved with the development of Cgreen for a few years, so when Software Craftsmanship Linköping asked me to do a TDD session for them I obviously choose that as my basis.

Cgreen is nice for allowing modern TDDing in C (and C++) using fluent API, mocks and the rest.

I talked and we coded. I selected the Texas Hold’em kata, which is interesting because of the multitude of dimensions that need to be covered. It is also a good kata to retry to experiment with different order of the tests. (Actually, I did it from memory and got it wrong, players have 2 private cards and community cards are delt until player folds. So the tests below are inaccurate.)

Here are the tests (of course I wrote one at a time with red-green-refactor in between):

#include <cgreen/cgreen.h>

#include "thm.h"

/* First test: drives infrastructure, format of cards, hands, return value is constant */
Ensure(single_player_with_highcard_wins) {
    assert_that(play_game("7D 9H AS 2H 8C 4D 5S\n"),
                is_equal_to_string("\n7D 9H AS 2H 8C 4D 5S High Card Winner"));
}

/* Second test: drives identifying cards with same value */
Ensure(single_player_with_pair_wins) {
    assert_that(play_game("7D 9H AS AH 8C 4D 5S\n"),
                is_equal_to_string("\n7D 9H AS AH 8C 4D 5S Pair Winner"));
}

/* Third test: next step along "same value" dimension */
Ensure(single_player_with_three_of_a_kind_wins) {
    assert_that(play_game("7D 8H AS AH 8C 4D 8S\n"),
                is_equal_to_string("\n7D 8H AS AH 8C 4D 8S Three Of A Kind Winner"));
}

/* Fourth test: drives finding a winner */
/* Actually also drove change of input/output format to make it easier to read */
Ensure(player_with_pair_wins_over_player_with_high_card) {
    assert_that(play_game("7D 9H AS 2H 8C 4D 5S\n"
                          "7H AH AS 2H 8C 4D 5S\n"),
                is_equal_to_string("\n7D 9H AS 2H 8C 4D 5S High Card"
                                   "\n7H AH AS 2H 8C 4D 5S Pair Winner"));
}

/* Fifth test: drives finding the actual winner even if it's not the first hand */
Ensure(player_with_high_card_loses_to_player_with_pair) {
    assert_that(play_game("AS 9H AS 2H TC 4D 5S\n"
                          "6D 7H AS 2H TC 4D 5S\n"),
                is_equal_to_string("\nAS 9H AS 2H TC 4D 5S Pair Winner"
                                   "\n6D 7H AS 2H TC 4D 5S High Card"));
}

/* Sixth test: handling three of a kind - still "same value" dimension */
Ensure(player_with_three_of_a_kind_wins_over_pair) {
    assert_that(play_game("AS 9H AS 2H 8C 4D 5S\n" /* Pair */
                          "2D 2S AS 2H 8C 4D 5S\n"), /* Three Of A Kind */
                is_equal_to_string("\nAS 9H AS 2H 8C 4D 5S Pair"
                                   "\n2D 2S AS 2H 8C 4D 5S Three Of A Kind Winner"));
}

/* Seventh test: handling three players - done in "number of players" dimension */
Ensure(player_with_three_of_a_kind_wins_over_high_card_and_pair) {
    assert_that(play_game("AS 9H AS 2H TC 4D 5S\n" /* Pair */
                          "2D 2S AS 2H TC 4D 5S\n" /* Three Of A Kind */
                          "6D 7H AS 2H TC 4D 5S\n"), /* High Card */
                is_equal_to_string("\nAS 9H AS 2H TC 4D 5S Pair"
                                   "\n2D 2S AS 2H TC 4D 5S Three Of A Kind Winner"
                                   "\n6D 7H AS 2H TC 4D 5S High Card"));
}

/* Eight test: handle four of a kind - done in "same value" dimension */
Ensure(player_with_four_of_a_kind_wins_over_three_of_a_kind) {
    assert_that(play_game("AS AH AS 2H AC 4D 5S\n" /* Four Of A Kind */
                          "2D 2S AS 2H AC 4D 5S\n"), /* Three Of A Kind */
                is_equal_to_string("\nAS AH AS 2H AC 4D 5S Four Of A Kind Winner"
                                   "\n2D 2S AS 2H AC 4D 5S Three Of A Kind"));
}

/* Ninth test: handle flush */
Ensure(player_with_flush_beats_high_card) {
    assert_that(play_game("QS KS AS 2S 4H 5S 9C\n" /* Flush */
                          "7D 8S AS 2S 4H 5S 9C\n"), /* High Card */
                is_equal_to_string("\nQS KS AS 2S 4H 5S 9C Flush Winner"
                                   "\n7D 8S AS 2S 4H 5S 9C High Card"));
}


/* Tenth test: start handling straight - match exact positions */
Ensure(player_with_straight_in_fixed_positions_beats_three_of_a_kind) {
    assert_that(play_game("2S KC AS 2S 2H 5S 9C\n" /* Three Of A Kind */
                          "3D 4S AS 2S 2H 5S 9C\n"), /* Straight */
                is_equal_to_string("\n2S KC AS 2S 2H 5S 9C Three Of A Kind"
                                   "\n3D 4S AS 2S 2H 5S 9C Straight Winner"));

}

/* Eleventh test: straight cont. - assume starts with 'A' find others */
Ensure(player_with_straight_starting_with_ace_beats_three_of_a_kind) {
    assert_that(play_game("2S KC AS 2S 2H 5S 9C\n" /* Three Of A Kind */
                          "4D 3S AS 2S 2H 5S 9C\n"), /* Straight */
                is_equal_to_string("\n2S KC AS 2S 2H 5S 9C Three Of A Kind"
                                   "\n4D 3S AS 2S 2H 5S 9C Straight Winner"));
}

/* Twelweth: straight cont. - starting with any value */
Ensure(player_with_straight_starting_with_three_beats_three_of_a_kind) {
    assert_that(play_game("2S 2C 3S 6S 2H 7S 9C\n" /* Three Of A Kind */
                          "4D 5S 3S 6S 2H 7S 9C\n"), /* Straight */
                is_equal_to_string("\n2S 2C 3S 6S 2H 7S 9C Three Of A Kind"
                                   "\n4D 5S 3S 6S 2H 7S 9C Straight Winner"));
}

/* Thirteenth: straight cont. - ending with Ace */
Ensure(player_with_straight_ending_in_ace_beats_high_card) {
    assert_that(play_game("AS 2C TS JS QH KS 8C\n" /* Straight */
                          "7H 5C TS JS QH KS 8C\n"), /* High Card */
                is_equal_to_string("\nAS 2C TS JS QH KS 8C Straight Winner"
                                   "\n7H 5C TS JS QH KS 8C High Card"));
}

/* Fourtheenth: straight cont. - starting in any position */
Ensure(player_with_straight_starting_in_any_position_beats_high_card) {
    assert_that(play_game("AS 2C 8S JS QH KS TC\n" /* Straight */
                          "7H 5C 8S JS QH KS TC\n"), /* High Card */
                is_equal_to_string("\nAS 2C 8S JS QH KS TC Straight Winner"
                                   "\n7H 5C 8S JS QH KS TC High Card"));
}

And here is the implementation (C string handling is a bit hairy…):

#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

static char *add_string_on_new_line(char *start, char *tail) {
    char *string;
    string = malloc(strlen(start) + 1 /*newline*/ + strlen(tail) + 1 /*null*/);
    string = strcpy(string, start);
    string = strcat(string, "\n");
    string = strcat(string, tail);
    return string;
}

static char *add_string_with_space(char *start, char *tail) {
    char *string;
    string = malloc(strlen(start) + 1 /*space*/ + strlen(tail) + 1 /*null*/);
    string = strcpy(string, start);
    string = strcat(string, " ");
    string = strcat(string, tail);
    return string;
}


static char color_of_card(char *hand, int card_number) {
    return hand[card_number*3+1];
}


static struct {char card; int value;} card_value_table[] = {
    {'A', 1},
    {'T', 10},
    {'J', 11},
    {'Q', 12},
    {'K', 13}
};

static int char2value(char card) {
    for (int i=0; i<sizeof(card_value_table)/sizeof(card_value_table[0]); i++)
        if (card_value_table[i].card == card)
            return card_value_table[i].value;
    return card-'0';
}


static char value_of_card(char *hand, int card_number) {
    return char2value(hand[card_number*3]);
}


static bool same_value_of_cards(char *hand, int i, int j) {
    return value_of_card(hand, i) == value_of_card(hand, j);
}


static bool has_four_of_a_kind(char *hand) {
    for (int i=0; i<7; i++)
        for (int j=i+1; j<7; j++)
            if (same_value_of_cards(hand, i, j))
                for (int k=j+1; k<7; k++)
                    if (same_value_of_cards(hand, i, k))
                        for (int l=k+1; l<7; l++)
                            if (same_value_of_cards(hand, i, l))
                                return true;
    return false;
}


static bool has_flush_in_color(char *hand, char color) {
	int count = 0;
    for (int i=0; i<7; i++)
        if (color_of_card(hand, i) == color)
            count++;
    return count == 5;
}


static bool has_flush(char *hand) {
    static char *colors = "SCHD";
    for (int color_index=0; color_index<4; color_index++) {
        if (has_flush_in_color(hand, colors[color_index]))
            return true;
    }
    return false;
}


static int card_with_value_exists(char *hand, char value) {
    for (int i=0; i<7; i++) {
        if (value_of_card(hand, i) == value)
            return true;
        if (value == 14 && value_of_card(hand, i) == 1)
            return true;
    }
	return false;
}


static bool four_next_cards_in_sequence_exists(char *hand, int start_value) {
    for (char value=start_value+1; value<start_value+5; value++)
        if (!card_with_value_exists(hand, value))
            return false;
	return true;
}


static bool has_straight(char *hand) {
    for (int card=0; card<7; card++) {
        int start_value = value_of_card(hand, card);
        if (four_next_cards_in_sequence_exists(hand, start_value))
            return true;
    }
    return false;
}


static bool has_three_of_a_kind(char *hand) {
    for (int i=0; i<7; i++)
        for (int j=i+1; j<7; j++)
            if (value_of_card(hand, i) == value_of_card(hand, j))
                for (int k=j+1; k<7; k++)
                    if (value_of_card(hand, i) == value_of_card(hand, k))
                        return true;
    return false;
}


static bool has_pair(char *hand) {
    for (int i=0; i<7; i++)
        for (int j=i+1; j<7; j++)
            if (value_of_card(hand, i) == value_of_card(hand, j))
                return true;
    return false;
}


static char *rank_names[] = {
    "High Card",
    "Pair",
    "Three Of A Kind",
    "Straight",
    "Flush",
    "Four Of A Kind"
};

static int rank_of_hand(char *hand) {
    if (has_four_of_a_kind(hand))
        return 5;
    else if (has_flush(hand))
        return 4;
    else if (has_straight(hand))
        return 3;
    else if (has_three_of_a_kind(hand))
        return 2;
    else if (has_pair(hand))
        return 1;
    else /* has_high_card() */
        return 0;
}


static char *get_hand(char *hands, int hand_number) {
	char *hand = strdup(&hands[hand_number*(3*7)]);
    hand[20] = '\0';            /* Remove newline */
	return hand;
}


static int find_winner(int rank[]) {
    int max_rank = 0;
    int winner = 0;
    for (int i=0; rank[i] != -1; i++)
        if (rank[i] >= max_rank) {
            max_rank = rank[i];
            winner = i;
        }
	return winner;
}


static int count_hands(char *hands) {
    int hand_count = 0;
    for (char *c = hands; *c != '\0'; hand_count += *c++ == '\n'?1:0);
	return hand_count;
}


static void get_hands(char *hand[], char *hands) {
    int hand_count = count_hands(hands);
    for (int i=0; i<hand_count; i++) {
        hand[i] = get_hand(hands, i);
    }
    hand[hand_count] = NULL;
}


static void rank_hands(int rank[], char *hand[]) {
    for (int hand_index=0; hand[hand_index] != NULL; hand_index++) {
        rank[hand_index] = rank_of_hand(hand[hand_index]);
        rank[hand_index+1] = -1;
    }
}


static char *result(char *hands[], int rank[], int winner) {
    char *string = "";
    for (int hand_index=0; hands[hand_index] != NULL; hand_index++) {
        string = add_string_on_new_line(string, hands[hand_index]);
        string = add_string_with_space(string, rank_names[rank[hand_index]]);
        if (winner == hand_index)
            string = add_string_with_space(string, "Winner");
    }
	return string;
}


char *play_game(char *deal) {
    char **hands;
    int *ranks;

    int hand_count = count_hands(deal);
    hands = malloc(hand_count*sizeof(char*)+1);
    ranks = malloc(hand_count*sizeof(int)+1);

    get_hands(hands, deal);
	rank_hands(ranks, hands);

    int winner = find_winner(ranks);

	return result(hands, ranks, winner);
}

Note that I’m not nearly done. That’s another thing with this kata, it’s long. E.g. there are a number of smaller refactorings that I’d like to do from this point.

What’s you suggestion for next action? Next test?

One Reply to “Texas Hold’em with Cgreen”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.