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”