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:

# http://www.fileformat.info/info/unicode/char/2666/index.htm
HEART = u'\u2665'
DIAMOND = u'\u2666'
SPADE = u'\u2660'
CLUB = u'\u2663'

# Suit name to symbol map
suits ={'heart':HEART,
        'diamond':DIAMOND,
        'spade':SPADE,
        'club':CLUB,
        'H':HEART,
        'D':DIAMOND,
        'S':SPADE,
        'C':CLUB}

for k,v in suits.items():
    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.

import random
from 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.

   # Shared tuple that stores the card ranks
ranks = ('A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K')

# shared tuple that stores the card suits
suits = ('D', 'H', 'C', 'S')


class Card(object):
    """
    This is a card object, it has a rank, a value, a suit, and a display.
    Value is an integer rank, suit and display are strings.
    """

# code for the class was inspired from:
# https://github.com/CJOlsen/Cribbage-Helper/blob/master/cribbage.py
# I have made some heavy modifications to the basic program

    def __init__(self, rank=None, suit=None):
        """
        Parameters
        ----------
        rank - a string representing the rank of the card: A, 2, 3, 4, 5, 6, 7,
               8, 9, 10, J, Q, K

        suit - a string representing the suit of the card D, H, C or S

        NOTE: If you send a combined string like '3H' or 'AS' in the rank slot.
              This will be split into the rank and suit. The order matters 'H3'
              or 'h3' or 'sa' won't be accepted.
        """

        if rank and suit:
            assert type(rank) == str and rank in ranks
            assert type(suit) == str and suit in suits

        elif rank:
            assert type(rank) == str
            assert len(rank) == 2

            r, s = rank.upper()

            # make sure the values are in the right order
            if r in ranks and s in suits:
                rank = r
                suit = s

            elif r in suits and s in ranks:
                rank = s
                suit = r

            else:
                raise ValueError('Rank and/or suit do not match!')

        else:
            raise ValueError('Rank and suit not properly set!')

        # at this point the rank and suit should be sorted
        self.rank = rank
        self.suit = suit

        if rank == 'A':
            self.value = 1

        elif rank in ('T', 'J', 'Q', 'K'):
            self.value = 10

        else:
            self.value = int(rank)

        self.display = rank + suit

        suit_symbols = {'H': u'\u2665',
                        'D': u'\u2666',
                        'S': u'\u2660',
                        'C': u'\u2663'}

        # TBW 2016-07-20
        # display the card with the rank and a graphical symbol
        # representing the suit
        self.cool_display = rank + suit_symbols[suit]

        # set the cards sorting order - useful for sorting a list of cards.
        rank_sort_order_map = {'A': 1,
                               '2': 2,
                               '3': 3,
                               '4': 4,
                               '5': 5,
                               '6': 6,
                               '7': 7,
                               '8': 8,
                               '9': 9,
                               'T': 10,
                               'J': 11,
                               'Q': 12,
                               'K': 13}

        self.sort_order = rank_sort_order_map[rank]

    def __eq__(self, other):
        """
        This overrides the == operator to check for equality
        """
        return self.__dict__ == other.__dict__

    def __add__(self, other):
        """
        """
        return self.value + other.value

    def __radd__(self, other):
        """
        """
        return self.value + other

    # TBW 2016-07-21
    def __lt__(self, other):
        """
        Make the item sortable
        """
        return self.sort_order < other.sort_order

    def __hash__(self):
        """
        Make the item hashable
        """
        return hash(self.display)

    def __str__(self):
        return self.cool_display

    def __repr__(self):
        # return "Card('{}', '{}')".format(self.rank, self.suit)
        return self.__str__()  # I don't need to produce the above...

Test Code:

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

Ouput:

3♠
3♠
3♠

MakeDeck

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

def make_deck():
    """
    Creates a deck of 52 cards. Returns the deck as a list
    """
    
    cards = []
    for p in product(ranks, suits):        
        cards.append(Card(*p))
    
    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.

class Hand(list):
    """
    A hand is a list of ***Card*** objects.
    """

    def __init__(self, *args):
        list.__init__(self, *args)

    def display(self):
        """
        Returns a list of ***Card*** objects in the hand in a format suitable
        for display: [AD, 1D, 3S,4C]
        """
        return [c.display for c in self]

    def cool_display(self):
        """
        Returns a list of ***Card*** objects in the hand in a format suitable
        for display: [A♦, 1♦, 3♠, 4♣]
        """
        return [c.cool_display for c in self]

    def value(self):
        """
        Returns the value of ***Card*** objects in the hand by summing
        the individual card values.
        A = 1
        J,Q,K = 10

        and the other cards are equal to the value of their rank.
        """
        return sum([c.value for c in self])

    def sorted(self):
        """
        Return a new ***Hand*** in sorted order.
        """
        return Hand(sorted(self, key=lambda c: c.sort_order, reverse=False))

    def every_combination(self, **kwargs):
        """
        A generator that will yield all possible combination of hands
        from the current hand.
        """

        if 'count' in kwargs:
            for combo in combinations(self, kwargs['count']):
                yield Hand(combo)
        else:
            for combo in chain.from_iterable(combinations(self, r)
                                             for r in range(len(self) + 1)):
                yield Hand(combo)   

Testing the Hand and Deck

deck = make_deck()
hand = Hand(random.sample(deck, 6))

print('Random Hand = {}'.format(hand.display()))
print('Random Hand = {}'.format(hand.cool_display()))
print('Sorted Hand = {}'.format(hand.sorted().cool_display()))
print('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

def find_fifteens_combos(hand):
    """
    A generator that takes a hand of cards and finds all of the combinations of
    cards that sum to 15. It returns a sub-hand containing the combination
    """
    for combo in hand.every_combination():
        if combo.value() == 15:
            yield combo


def count_fifteens(hand):
    """
    Counts the number of combinations within the hand of cards that sum to 15.
    Each combination is worth 2 points.

    Returns a tuple containing the total number of combinations and the total
    points.
    """
    combos = list(find_fifteens_combos(hand))
    return len(combos), len(combos)*2

Test code:

hand = Hand(random.sample(deck, 5))
print('Hand = {}'.format(hand.sorted().cool_display()))

print('{} Fifteens for {}.'.format(*count_fifteens(hand)))

# display the combinations
for combo in find_fifteens_combos(hand): 
    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

def find_pairs(hand):
    """
    A generator that will iterate through all of the combinations and yield
    pairs of cards.
    """
    for combo in hand.every_combination(count=2):
        if combo[0].rank == combo[1].rank:
            yield combo


def count_pairs(hand):
    """
    Returns the score due to all the pairs found in the hand. Each pair is
    worth 3 points.
    """
    pairs = list(find_pairs(hand))
    return len(pairs), len(pairs)*2

Test code:

hand = Hand([Card('5','D'), Card('5', 'S'), Card('5', 'C'), Card('J', 'S'), Card('A','C')])
print('Hand = {}'.format(hand.sorted().cool_display()))
print()
print('{} Fifteens for {}.'.format(*count_fifteens(hand)))
print('{} Pairs for    {}.'.format(*count_pairs(hand)))
print()

print('Fifteens====')
for combo in find_fifteens_combos(hand): 
    print('{} = 15'.format(', '.join(combo.cool_display())))   

print()
print('Pairs====')
# display the pairs
for combo in find_pairs(hand): 
    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

def find_runs(hand):
    """
    A generator that takes a hand of cards and finds all runs of 3 or more
    cards. Returns each set of cards that makes a run.
    """
    runs = []
    for combo in chain.from_iterable(combinations(hand, r)
                                     for r in range(3, len(hand)+1)):

        for k, g in groupby(enumerate(Hand(combo).sorted()),
                            lambda ix: ix[0] - ix[1].sort_order):

            # strip out the enumeration and get the cards in the group
            new_hand = Hand([i[1] for i in g])
            if len(new_hand) < 3:
                continue

            m = set(new_hand)

            # check to see if the new run is a subset of an existing run
            if any([m.issubset(s) for s in runs]):
                continue

            # if the new run is a super set of previous runs, we need to remove
            # them
            l = [m.issuperset(s) for s in runs]
            if any(l):
                runs = [r for r, t in zip(runs, l) if not t]

            if m not in runs:
                runs.append(m)

    return [Hand(list(r)).sorted() for r in runs]


def count_runs(hand):
    """
    Count the number of points in all the runs. 1 point per card in the run
    (at least 3 cards).
    """
    runs = list(find_runs(hand))
    return len(runs), sum([len(r) for r in runs])

Test code:

hands = [Hand([Card('2','D'), Card('3', 'D'), Card('4', 'D'), Card('8', 'D'), Card('5','D')]),
         Hand([Card('2','D'), Card('3', 'D'), Card('3', 'S'), Card('3', 'C'), Card('4','D')]),
         Hand([Card('2','D'), Card('4', 'D'), Card('6', 'H'), Card('8', 'S'), Card('9','D')])]

for hand in hands:
    print('Hand = {}'.format(hand.sorted().cool_display()))
    print()

    print('{} Runs for     {}.'.format(*count_runs(hand)))
    print()

    print('Runs====')
    for combo in find_runs(hand):
        print(combo.cool_display())
    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

def count_flushes(hand, cut, is_crib=False):
    """
    Scores the points for flushes.
    """

    assert len(hand) == 4

    m = set([c.suit for c in hand])
    if len(m) == 1:
        score = 4

        if cut and m.pop() == cut.suit:
            score += 1

        if is_crib:
            # The crib can only score a flush if all the cards
            # in the crib are the same suit and the cut card
            # is the same suit. Otherwise, a flush isn't counted.
            if score != 5:
                return 0

        return score

    else:
        return 0

Test code:

m = [Card('2','D'), Card('3', 'D'), Card('4', 'D'), Card('8', 'D')]
hand = Hand(m)
cut = Card('5','D')
full_hand = Hand(m + [cut])

print('Hand      = {}'.format(hand.sorted().cool_display()))
print('Cut       = {}'.format(cut.cool_display))
print('Full Hand = {}'.format(full_hand.sorted().cool_display()))
print()
print('{} Fifteens for {}.'.format(*count_fifteens(full_hand)))
print('{} Pairs for    {}.'.format(*count_pairs(full_hand)))
print('{} Runs for     {}.'.format(*count_runs(full_hand)))
print('Flush for      {}.'.format(count_flushes(hand, cut)))
print()

print('Fifteens====')
for combo in find_fifteens_combos(hand): 
    print('{} = 15'.format(', '.join(combo.cool_display())))   

print()
print('Pairs====')
for combo in find_pairs(hand): 
    print('{}'.format(', '.join(combo.cool_display())))    
    
print()
print('Runs====')
for combo in find_runs(hand):
    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

def count_nobs(hand, cut):
    """
    Takes a 4 card hand and a cut card. If the hand contains a jack and it is
    the same suit as the cut card than a point is scored. This is called nobs.
    """
    assert len(hand) == 4

    if not cut:
        return 0

    if any([c.suit == cut.suit and c.rank == 'J' for c in hand]):
        return 1

    else:
        return 0

Test code:

m = [Card('2','D'), Card('3', 'D'), Card('J', 'D'), Card('8', 'D')]
hand = Hand(m)
cut = Card('5','D')
full_hand = Hand(m + [cut])

print('Hand      = {}'.format(hand.sorted().cool_display()))
print('Cut       = {}'.format(cut.cool_display))
print('Full Hand = {}'.format(full_hand.sorted().cool_display()))
print()

total_count = 0
number, value = count_fifteens(full_hand)
total_count += value
print('{} Fifteens for {}'.format(number, value))

number, value = count_pairs(full_hand)
total_count += value
print('{} Pairs for    {}'.format(number, value))

number, value = count_runs(full_hand)
total_count += value
print('{} Runs for     {}'.format(number, value))

value = count_flushes(hand, cut)
total_count += value
print('Flush for      {}'.format(value))

value = count_nobs(hand, cut)
total_count += value
print('Nobs for       {}'.format(value))
print('------------------')
print('Total          {}'.format(total_count))
print()

print('Fifteens====')
for combo in find_fifteens_combos(hand): 
    print('{} = 15'.format(', '.join(combo.cool_display())))   

print()
print('Pairs====')
for combo in find_pairs(hand): 
    print('{}'.format(', '.join(combo.cool_display())))    
    
print()
print('Runs====')
for combo in find_runs(hand):
    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

def score_hand(hand, cut, **kwargs):
    """
    Takes a 4 card crib hand and the cut card and scores it.

    Returns a dictionary containing the various items
    """

    # defaults
    is_crib = False if 'is_crib' not in kwargs else kwargs['is_crib']

    full_hand = Hand(hand + [cut]) if cut else hand
    scores = {}  # contain the scores
    count = {}  # contain the counts for items that can hit multiple times

    number, value = count_fifteens(full_hand)
    count['fifteen'] = number
    scores['fifteen'] = value

    number, value = count_pairs(full_hand)
    count['pair'] = number
    scores['pair'] = value

    number, value = count_runs(full_hand)
    count['run'] = number
    scores['run'] = value

    scores['flush'] = count_flushes(hand, cut, is_crib)
    scores['nobs'] = count_nobs(hand, cut)

    return scores, count
def display_points(hand, cut, scores, counts):
    print('Hand      = {}'.format(','.join(hand.sorted().cool_display())))
    print('Cut       = {}'.format(cut.cool_display if cut else 'N/A'))
    print()

    print('{} Fifteens for {}'.format(counts['fifteen'], scores['fifteen']))
    print('{} Pairs for    {}'.format(counts['pair'], scores['pair']))
    print('{} Runs for     {}'.format(counts['run'], scores['run']))
    print('Flush for      {}'.format(scores['flush']))
    print('Nobs for       {}'.format(scores['nobs']))
    print('-----------------')
    print('Total          {}'.format(sum([v for k, v in scores.items()])))
    print()

    full_hand = Hand(hand + [cut]) if cut else hand
    print('Fifteens====')
    for combo in find_fifteens_combos(full_hand):
        print('{} = 15'.format(', '.join(combo.cool_display())))

    print()
    print('Pairs====')
    for combo in find_pairs(full_hand):
        print('{}'.format(', '.join(combo.cool_display())))

    print()
    print('Runs====')
    for combo in find_runs(full_hand):
        print(', '.join(combo.cool_display()))

Test code:

hand = Hand([Card('5','C'), Card('5', 'S'), Card('J', 'H'), Card('J', 'C'), Card('J','S')])
print('Hand = {}'.format(hand.sorted().cool_display()))

print('{} Fifteens for {}.'.format(*count_fifteens(hand)))

# display the combinations
for combo in find_fifteens_combos(hand): 
    print('{} = 15'.format(', '.join(combo.sorted().cool_display())))    
    
print('--------')
hand = Hand([Card('5','C'), Card('5', 'S'), Card('J', 'H'), Card('J', 'C')])
cut = Card('J','S')
scores, counts = score_hand(hand, cut)
display_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====

Troy Williams

My name is Troy Williams and I am a professional mining engineer and programmer writing software for the mining industry.