Updated on 2021-07-24

There is a newer article with newer code that you can read.

Cribbage - Scoring#

In this article, I am going to explore python code that can construct a cribbage hand and score it.

Cribbage Scoring Rules:

15: Any combination of cards adding up to 15 pips scores 2 points. For example, king, jack, five, five would score 10 points altogether: 8 points for four fifteens, since the king and the jack can each be paired with either of the fives, plus 2 more points for the pair of fives. You would say “Fifteen two, fifteen four, fifteen six, fifteen eight and a pair makes ten”.

Pair: A pair of cards of the same rank score 2 points. Three cards of the same rank contain 3 different pairs and thus score a total of 6 points for pair royal. Four of a kind contain 6 pairs and so score 12 points.

Run: Three cards of consecutive rank (irrespective of suit), such as ace-2-3, score 3 points for a run. A hand such as 6-7-7-8 contains two runs of 3 (as well as two fifteens and a pair) and so would score 12 altogether. A run of four cards, such as 9-10-J-Q scores 4 points (this is slightly illogical - you might expect it to score 6 because it contains two runs of 3, but it doesn’t. The runs of 3 within it don’t count - you just get 4), and a run of five cards scores 5.

Flush: If all four cards of the hand are the same suit, 4 points are scored for flush. If the start card is the same suit as well, the flush is worth 5 points. There is no score for having 3 hand cards and the start all the same suit. Note also that there is no score for flush during the play - it only counts in the show.

One For His Nob: If the hand contains the jack of the same suit as the start card, you peg One for his nob (sometimes known, especially in North America, as “one for his nobs” or “one for his nibs”).

Conventions#

The cards in a deck will be represented by two characters, the face value (A,2,3,4,5,6,7,8,9,10,J,Q,K) and the suit (S, H, D, C). So the king of spades would be: KS and the 2 of diamonds would be: 2D. Face cards have a point value of 10, the ace is 1 point and all other cards represent their value.

The player has 4 cards in their hand and the cut card. The program should be able to accurately count the score.

The following hand would be 1 pair for 2 points: 2D, 2S, KD, QD: 4C

Code#

The code was inspired by code from here. I have heavily modified the routines and methods.

I wanted to be able to show symbols for the different suits. The following bit of code is a proof of concept:

 1# http://www.fileformat.info/info/unicode/char/2666/index.htm
 2HEART = u'\u2665'
 3DIAMOND = u'\u2666'
 4SPADE = u'\u2660'
 5CLUB = u'\u2663'
 6
 7# Suit name to symbol map
 8suits ={'heart':HEART,
 9        'diamond':DIAMOND,
10        'spade':SPADE,
11        'club':CLUB,
12        'H':HEART,
13        'D':DIAMOND,
14        'S':SPADE,
15        'C':CLUB}
16
17for k,v in suits.items():
18    print('{:7} = {}'.format(k,v))

Output:

heart   = ♥
diamond = ♦
spade   = ♠
club    = ♣
H       = ♥
D       = ♦
S       = ♠
C       = ♣

Imports#

The imports contain the modules that will be used by the methods.

1import random
2from itertools import chain, combinations, product, groupby

Card Class#

The card class is the basis of all the other methods. It makes counting and displays much easier.

  1   # Shared tuple that stores the card ranks
  2ranks = ('A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K')
  3
  4# shared tuple that stores the card suits
  5suits = ('D', 'H', 'C', 'S')
  6
  7
  8class Card(object):
  9    """
 10    This is a card object, it has a rank, a value, a suit, and a display.
 11    Value is an integer rank, suit and display are strings.
 12    """
 13
 14# code for the class was inspired from:
 15# https://github.com/CJOlsen/Cribbage-Helper/blob/master/cribbage.py
 16# I have made some heavy modifications to the basic program
 17
 18    def __init__(self, rank=None, suit=None):
 19        """
 20        Parameters
 21        ----------
 22        rank - a string representing the rank of the card: A, 2, 3, 4, 5, 6, 7,
 23               8, 9, 10, J, Q, K
 24
 25        suit - a string representing the suit of the card D, H, C or S
 26
 27        NOTE: If you send a combined string like '3H' or 'AS' in the rank slot.
 28              This will be split into the rank and suit. The order matters 'H3'
 29              or 'h3' or 'sa' won't be accepted.
 30        """
 31
 32        if rank and suit:
 33            assert type(rank) == str and rank in ranks
 34            assert type(suit) == str and suit in suits
 35
 36        elif rank:
 37            assert type(rank) == str
 38            assert len(rank) == 2
 39
 40            r, s = rank.upper()
 41
 42            # make sure the values are in the right order
 43            if r in ranks and s in suits:
 44                rank = r
 45                suit = s
 46
 47            elif r in suits and s in ranks:
 48                rank = s
 49                suit = r
 50
 51            else:
 52                raise ValueError('Rank and/or suit do not match!')
 53
 54        else:
 55            raise ValueError('Rank and suit not properly set!')
 56
 57        # at this point the rank and suit should be sorted
 58        self.rank = rank
 59        self.suit = suit
 60
 61        if rank == 'A':
 62            self.value = 1
 63
 64        elif rank in ('T', 'J', 'Q', 'K'):
 65            self.value = 10
 66
 67        else:
 68            self.value = int(rank)
 69
 70        self.display = rank + suit
 71
 72        suit_symbols = {'H': u'\u2665',
 73                        'D': u'\u2666',
 74                        'S': u'\u2660',
 75                        'C': u'\u2663'}
 76
 77        # TBW 2016-07-20
 78        # display the card with the rank and a graphical symbol
 79        # representing the suit
 80        self.cool_display = rank + suit_symbols[suit]
 81
 82        # set the cards sorting order - useful for sorting a list of cards.
 83        rank_sort_order_map = {'A': 1,
 84                               '2': 2,
 85                               '3': 3,
 86                               '4': 4,
 87                               '5': 5,
 88                               '6': 6,
 89                               '7': 7,
 90                               '8': 8,
 91                               '9': 9,
 92                               'T': 10,
 93                               'J': 11,
 94                               'Q': 12,
 95                               'K': 13}
 96
 97        self.sort_order = rank_sort_order_map[rank]
 98
 99    def __eq__(self, other):
100        """
101        This overrides the == operator to check for equality
102        """
103        return self.__dict__ == other.__dict__
104
105    def __add__(self, other):
106        """
107        """
108        return self.value + other.value
109
110    def __radd__(self, other):
111        """
112        """
113        return self.value + other
114
115    # TBW 2016-07-21
116    def __lt__(self, other):
117        """
118        Make the item sortable
119        """
120        return self.sort_order < other.sort_order
121
122    def __hash__(self):
123        """
124        Make the item hashable
125        """
126        return hash(self.display)
127
128    def __str__(self):
129        return self.cool_display
130
131    def __repr__(self):
132        # return "Card('{}', '{}')".format(self.rank, self.suit)
133        return self.__str__()  # I don't need to produce the above...

Test Code:

1print(Card('3S').cool_display)
2print(Card('s3').cool_display)
3print(Card('3', 'S').cool_display)

Ouput:

3♠
3♠
3♠

MakeDeck#

The make deck method will create a deck of 52 cards in sorted order.

 1def make_deck():
 2    """
 3    Creates a deck of 52 cards. Returns the deck as a list
 4    """
 5
 6    cards = []
 7    for p in product(ranks, suits):
 8        cards.append(Card(*p))
 9
10    return cards

Hand#

We need to model a player hand. The hand object is generalized and should work with any number of cards in a players hand, from 4 to 6.

 1class Hand(list):
 2    """
 3    A hand is a list of ***Card*** objects.
 4    """
 5
 6    def __init__(self, *args):
 7        list.__init__(self, *args)
 8
 9    def display(self):
10        """
11        Returns a list of ***Card*** objects in the hand in a format suitable
12        for display: [AD, 1D, 3S,4C]
13        """
14        return [c.display for c in self]
15
16    def cool_display(self):
17        """
18        Returns a list of ***Card*** objects in the hand in a format suitable
19        for display: [A♦, 1♦, 3♠, 4♣]
20        """
21        return [c.cool_display for c in self]
22
23    def value(self):
24        """
25        Returns the value of ***Card*** objects in the hand by summing
26        the individual card values.
27        A = 1
28        J,Q,K = 10
29
30        and the other cards are equal to the value of their rank.
31        """
32        return sum([c.value for c in self])
33
34    def sorted(self):
35        """
36        Return a new ***Hand*** in sorted order.
37        """
38        return Hand(sorted(self, key=lambda c: c.sort_order, reverse=False))
39
40    def every_combination(self, **kwargs):
41        """
42        A generator that will yield all possible combination of hands
43        from the current hand.
44        """
45
46        if 'count' in kwargs:
47            for combo in combinations(self, kwargs['count']):
48                yield Hand(combo)
49        else:
50            for combo in chain.from_iterable(combinations(self, r)
51                                             for r in range(len(self) + 1)):
52                yield Hand(combo)

Testing the Hand and Deck#

1deck = make_deck()
2hand = Hand(random.sample(deck, 6))
3
4print('Random Hand = {}'.format(hand.display()))
5print('Random Hand = {}'.format(hand.cool_display()))
6print('Sorted Hand = {}'.format(hand.sorted().cool_display()))
7print('Hand Sum  = {}'.format(hand.value()))

Output:

Random Hand = ['2D', '2H', 'TC', 'QC', '8D', 'JD']
Random Hand = ['2♦', '2♥', 'T♣', 'Q♣', '8♦', 'J♦']
Sorted Hand = ['2♦', '2♥', '8♦', 'T♣', 'J♦', 'Q♣']
Hand Sum  = 42

Scoring Fifteens#

 1def find_fifteens_combos(hand):
 2    """
 3    A generator that takes a hand of cards and finds all of the combinations of
 4    cards that sum to 15. It returns a sub-hand containing the combination
 5    """
 6    for combo in hand.every_combination():
 7        if combo.value() == 15:
 8            yield combo
 9
10
11def count_fifteens(hand):
12    """
13    Counts the number of combinations within the hand of cards that sum to 15.
14    Each combination is worth 2 points.
15
16    Returns a tuple containing the total number of combinations and the total
17    points.
18    """
19    combos = list(find_fifteens_combos(hand))
20    return len(combos), len(combos)*2

Test code:

1hand = Hand(random.sample(deck, 5))
2print('Hand = {}'.format(hand.sorted().cool_display()))
3
4print('{} Fifteens for {}.'.format(*count_fifteens(hand)))
5
6# display the combinations
7for combo in find_fifteens_combos(hand):
8    print('{} = 15'.format(', '.join(combo.sorted().cool_display())))

Output:

Hand = ['A♣', '2♥', '4♠', 'T♦', 'K♠']
2 Fifteens for 4.
A♣, 4♠, K♠ = 15
A♣, 4♠, T♦ = 15

Scoring Pairs#

 1def find_pairs(hand):
 2    """
 3    A generator that will iterate through all of the combinations and yield
 4    pairs of cards.
 5    """
 6    for combo in hand.every_combination(count=2):
 7        if combo[0].rank == combo[1].rank:
 8            yield combo
 9
10
11def count_pairs(hand):
12    """
13    Returns the score due to all the pairs found in the hand. Each pair is
14    worth 3 points.
15    """
16    pairs = list(find_pairs(hand))
17    return len(pairs), len(pairs)*2

Test code:

 1hand = Hand([Card('5','D'), Card('5', 'S'), Card('5', 'C'), Card('J', 'S'), Card('A','C')])
 2print('Hand = {}'.format(hand.sorted().cool_display()))
 3print()
 4print('{} Fifteens for {}.'.format(*count_fifteens(hand)))
 5print('{} Pairs for    {}.'.format(*count_pairs(hand)))
 6print()
 7
 8print('Fifteens====')
 9for combo in find_fifteens_combos(hand):
10    print('{} = 15'.format(', '.join(combo.cool_display())))
11
12print()
13print('Pairs====')
14# display the pairs
15for combo in find_pairs(hand):
16    print('{}'.format(', '.join(combo.cool_display())))

Output:

Hand = ['A♣', '5♦', '5♠', '5♣', 'J♠']

4 Fifteens for 8.
3 Pairs for    6.

Fifteens====
5♦, J♠ = 15
5♠, J♠ = 15
5♣, J♠ = 15
5♦, 5♠, 5♣ = 15

Pairs====
5♦, 5♠
5♦, 5♣
5♠, 5♣

Scoring Runs#

 1def find_runs(hand):
 2    """
 3    A generator that takes a hand of cards and finds all runs of 3 or more
 4    cards. Returns each set of cards that makes a run.
 5    """
 6    runs = []
 7    for combo in chain.from_iterable(combinations(hand, r)
 8                                     for r in range(3, len(hand)+1)):
 9
10        for k, g in groupby(enumerate(Hand(combo).sorted()),
11                            lambda ix: ix[0] - ix[1].sort_order):
12
13            # strip out the enumeration and get the cards in the group
14            new_hand = Hand([i[1] for i in g])
15            if len(new_hand) < 3:
16                continue
17
18            m = set(new_hand)
19
20            # check to see if the new run is a subset of an existing run
21            if any([m.issubset(s) for s in runs]):
22                continue
23
24            # if the new run is a super set of previous runs, we need to remove
25            # them
26            l = [m.issuperset(s) for s in runs]
27            if any(l):
28                runs = [r for r, t in zip(runs, l) if not t]
29
30            if m not in runs:
31                runs.append(m)
32
33    return [Hand(list(r)).sorted() for r in runs]
34
35
36def count_runs(hand):
37    """
38    Count the number of points in all the runs. 1 point per card in the run
39    (at least 3 cards).
40    """
41    runs = list(find_runs(hand))
42    return len(runs), sum([len(r) for r in runs])

Test code:

 1hands = [Hand([Card('2','D'), Card('3', 'D'), Card('4', 'D'), Card('8', 'D'), Card('5','D')]),
 2         Hand([Card('2','D'), Card('3', 'D'), Card('3', 'S'), Card('3', 'C'), Card('4','D')]),
 3         Hand([Card('2','D'), Card('4', 'D'), Card('6', 'H'), Card('8', 'S'), Card('9','D')])]
 4
 5for hand in hands:
 6    print('Hand = {}'.format(hand.sorted().cool_display()))
 7    print()
 8
 9    print('{} Runs for     {}.'.format(*count_runs(hand)))
10    print()
11
12    print('Runs====')
13    for combo in find_runs(hand):
14        print(combo.cool_display())
15    print()

Output:

Hand = ['2♦', '3♦', '4♦', '5♦', '8♦']

1 Runs for     4.

Runs====
['2♦', '3♦', '4♦', '5♦']

Hand = ['2♦', '3♦', '3♠', '3♣', '4♦']

3 Runs for     9.

Runs====
['2♦', '3♦', '4♦']
['2♦', '3♠', '4♦']
['2♦', '3♣', '4♦']

Hand = ['2♦', '4♦', '6♥', '8♠', '9♦']

0 Runs for     0.

Runs====

Flushes#

A four-card flush scores four points, unless in the crib. A four-card flush occurs when all of the cards in a player’s hand are the same suit, and the start card is a different suit. In the crib, a four-card flush scores no points. A five-card flush scores five points.

Basically, this means that we have to take into account the cards in hand and the card in the cut. A flush is only counted if the 4 hand cards are the same suit for 4 points, If the cut card is the same, an additional point is awarded.

In the crib, a four-card flush isn’t counted. If the cut card is the same, then the flush is counted for 5 points

 1def count_flushes(hand, cut, is_crib=False):
 2    """
 3    Scores the points for flushes.
 4    """
 5
 6    assert len(hand) == 4
 7
 8    m = set([c.suit for c in hand])
 9    if len(m) == 1:
10        score = 4
11
12        if cut and m.pop() == cut.suit:
13            score += 1
14
15        if is_crib:
16            # The crib can only score a flush if all the cards
17            # in the crib are the same suit and the cut card
18            # is the same suit. Otherwise, a flush isn't counted.
19            if score != 5:
20                return 0
21
22        return score
23
24    else:
25        return 0

Test code:

 1m = [Card('2','D'), Card('3', 'D'), Card('4', 'D'), Card('8', 'D')]
 2hand = Hand(m)
 3cut = Card('5','D')
 4full_hand = Hand(m + [cut])
 5
 6print('Hand      = {}'.format(hand.sorted().cool_display()))
 7print('Cut       = {}'.format(cut.cool_display))
 8print('Full Hand = {}'.format(full_hand.sorted().cool_display()))
 9print()
10print('{} Fifteens for {}.'.format(*count_fifteens(full_hand)))
11print('{} Pairs for    {}.'.format(*count_pairs(full_hand)))
12print('{} Runs for     {}.'.format(*count_runs(full_hand)))
13print('Flush for      {}.'.format(count_flushes(hand, cut)))
14print()
15
16print('Fifteens====')
17for combo in find_fifteens_combos(hand):
18    print('{} = 15'.format(', '.join(combo.cool_display())))
19
20print()
21print('Pairs====')
22for combo in find_pairs(hand):
23    print('{}'.format(', '.join(combo.cool_display())))
24
25print()
26print('Runs====')
27for combo in find_runs(hand):
28    print(combo.cool_display())

Output:

Hand      = ['2♦', '3♦', '4♦', '8♦']
Cut       = 5♦
Full Hand = ['2♦', '3♦', '4♦', '5♦', '8♦']

2 Fifteens for 4.
0 Pairs for    0.
1 Runs for     4.
Flush for      5.

Fifteens====
3♦, 4♦, 8♦ = 15

Pairs====

Runs====
['2♦', '3♦', '4♦']

Nobs#

 1def count_nobs(hand, cut):
 2    """
 3    Takes a 4 card hand and a cut card. If the hand contains a jack and it is
 4    the same suit as the cut card than a point is scored. This is called nobs.
 5    """
 6    assert len(hand) == 4
 7
 8    if not cut:
 9        return 0
10
11    if any([c.suit == cut.suit and c.rank == 'J' for c in hand]):
12        return 1
13
14    else:
15        return 0

Test code:

 1m = [Card('2','D'), Card('3', 'D'), Card('J', 'D'), Card('8', 'D')]
 2hand = Hand(m)
 3cut = Card('5','D')
 4full_hand = Hand(m + [cut])
 5
 6print('Hand      = {}'.format(hand.sorted().cool_display()))
 7print('Cut       = {}'.format(cut.cool_display))
 8print('Full Hand = {}'.format(full_hand.sorted().cool_display()))
 9print()
10
11total_count = 0
12number, value = count_fifteens(full_hand)
13total_count += value
14print('{} Fifteens for {}'.format(number, value))
15
16number, value = count_pairs(full_hand)
17total_count += value
18print('{} Pairs for    {}'.format(number, value))
19
20number, value = count_runs(full_hand)
21total_count += value
22print('{} Runs for     {}'.format(number, value))
23
24value = count_flushes(hand, cut)
25total_count += value
26print('Flush for      {}'.format(value))
27
28value = count_nobs(hand, cut)
29total_count += value
30print('Nobs for       {}'.format(value))
31print('------------------')
32print('Total          {}'.format(total_count))
33print()
34
35print('Fifteens====')
36for combo in find_fifteens_combos(hand):
37    print('{} = 15'.format(', '.join(combo.cool_display())))
38
39print()
40print('Pairs====')
41for combo in find_pairs(hand):
42    print('{}'.format(', '.join(combo.cool_display())))
43
44print()
45print('Runs====')
46for combo in find_runs(hand):
47    print(combo.cool_display())

Output:

Hand      = ['2♦', '3♦', '8♦', 'J♦']
Cut       = 5♦
Full Hand = ['2♦', '3♦', '5♦', '8♦', 'J♦']

3 Fifteens for 6
0 Pairs for    0
0 Runs for     0
Flush for      5
Nobs for       1
------------------
Total          12

Fifteens====
2♦, 3♦, J♦ = 15

Pairs====

Runs====

Putting it all together - Scoring the Hand#

 1def score_hand(hand, cut, **kwargs):
 2    """
 3    Takes a 4 card crib hand and the cut card and scores it.
 4
 5    Returns a dictionary containing the various items
 6    """
 7
 8    # defaults
 9    is_crib = False if 'is_crib' not in kwargs else kwargs['is_crib']
10
11    full_hand = Hand(hand + [cut]) if cut else hand
12    scores = {}  # contain the scores
13    count = {}  # contain the counts for items that can hit multiple times
14
15    number, value = count_fifteens(full_hand)
16    count['fifteen'] = number
17    scores['fifteen'] = value
18
19    number, value = count_pairs(full_hand)
20    count['pair'] = number
21    scores['pair'] = value
22
23    number, value = count_runs(full_hand)
24    count['run'] = number
25    scores['run'] = value
26
27    scores['flush'] = count_flushes(hand, cut, is_crib)
28    scores['nobs'] = count_nobs(hand, cut)
29
30    return scores, count
 1def display_points(hand, cut, scores, counts):
 2    print('Hand      = {}'.format(','.join(hand.sorted().cool_display())))
 3    print('Cut       = {}'.format(cut.cool_display if cut else 'N/A'))
 4    print()
 5
 6    print('{} Fifteens for {}'.format(counts['fifteen'], scores['fifteen']))
 7    print('{} Pairs for    {}'.format(counts['pair'], scores['pair']))
 8    print('{} Runs for     {}'.format(counts['run'], scores['run']))
 9    print('Flush for      {}'.format(scores['flush']))
10    print('Nobs for       {}'.format(scores['nobs']))
11    print('-----------------')
12    print('Total          {}'.format(sum([v for k, v in scores.items()])))
13    print()
14
15    full_hand = Hand(hand + [cut]) if cut else hand
16    print('Fifteens====')
17    for combo in find_fifteens_combos(full_hand):
18        print('{} = 15'.format(', '.join(combo.cool_display())))
19
20    print()
21    print('Pairs====')
22    for combo in find_pairs(full_hand):
23        print('{}'.format(', '.join(combo.cool_display())))
24
25    print()
26    print('Runs====')
27    for combo in find_runs(full_hand):
28        print(', '.join(combo.cool_display()))

Test code:

 1hand = Hand([Card('5','C'), Card('5', 'S'), Card('J', 'H'), Card('J', 'C'), Card('J','S')])
 2print('Hand = {}'.format(hand.sorted().cool_display()))
 3
 4print('{} Fifteens for {}.'.format(*count_fifteens(hand)))
 5
 6# display the combinations
 7for combo in find_fifteens_combos(hand):
 8    print('{} = 15'.format(', '.join(combo.sorted().cool_display())))
 9
10print('--------')
11hand = Hand([Card('5','C'), Card('5', 'S'), Card('J', 'H'), Card('J', 'C')])
12cut = Card('J','S')
13scores, counts = score_hand(hand, cut)
14display_points(hand, cut, scores, counts)

Output:

Hand = ['5♣', '5♠', 'J♥', 'J♣', 'J♠']
6 Fifteens for 12.
5♣, J♥ = 15
5♣, J♣ = 15
5♣, J♠ = 15
5♠, J♥ = 15
5♠, J♣ = 15
5♠, J♠ = 15
--------
Hand      = 5♣,5♠,J♥,J♣
Cut       = J♠

6 Fifteens for 12
4 Pairs for    8
0 Runs for     0
Flush for      0
Nobs for       0
-----------------
Total          20

Fifteens====
5♣, J♥ = 15
5♣, J♣ = 15
5♣, J♠ = 15
5♠, J♥ = 15
5♠, J♣ = 15
5♠, J♠ = 15

Pairs====
5♣, 5♠
J♥, J♣
J♥, J♠
J♣, J♠

Runs====