Cribbage - Expected Average

References

Imports

You will have to make a module called cribbage.py from the previous article making the cribbage objects.

import random
import json
from itertools import combinations

from cribbage import Card, Hand, score_hand, display_points, make_deck, make_hand

Average Hand Value

In this section we'll explore how to calculate the average hand value of any particular 4 card hand based on what the cut card could potentially be. Given the fact that we are dealt 6 cards, what is the best hand, on average, that can be made with 4 cards and the potential cut card.

Given that we are dealt the following hand: 2C 3H 4D 5D 5S JS

Let's analyze the following 4 card hand: 3C, 4H, 5C, 5D, what can we expect in terms of the cut card? That is, based on what could be turned up as the cut card what kind of points can be expected, on average?

We need to remove the cards that we know about from the deck so that we have:

$$52 - 6 = 46$$

Starter Card Frequency Hand Value Total
A 4 10 40
2 3 12 36
3 3 20 60
4 3 16 48
5 2 17 34
6 4 14 56
7 4 12 48
8 4 10 40
9 4 8 32
10 4 12 48
J 3 12 36
Q 4 12 48
K 4 12 48
Total: 574

for the 4 card hand, the average points would be:

$$\frac{574}{46} = 12.48$$

Table columns:

  • Starter - This column simply lists the potential cut card.
  • Card Frequency - This is the number of cards in the deck that are left based on the 6 cards you are dealt. For example, if I was dealt 2 aces, that would mean there are only two aces left in the deck.
  • Hand Value - This is the value of the hand including the cut/starter card
  • Total - This is the product of the card frequency and hand value. Basically it is a weighting factor.

You sum the value in the total column and divide by the number of cards left in the deck, $$52 - 6 = 46$$.

deck = make_deck()

# shuffle the deck
random.shuffle(deck)

# extract the first 6 cards (removing them from the deck)
hand = make_hand(deck, 6)

random.shuffle(hand)
candidate = Hand(hand[:4])
discard =  hand[-2:]
print('Candidate Hand: ',candidate)
print('Discard: ', discard)
print()

total = 0.0
for i, cut in enumerate(sorted(deck), 1):
    scores, counts = score_hand(candidate, cut)    
    value = sum(scores.values())
    print('{:>2} Cards = {}, Cut = {} - Points = {}'.format(i, candidate.sorted(), cut, value))
    total += value
    
    
print()
print('Average hand value = {}'.format(total/len(deck)))

Output:

Candidate Hand:  [6♣, K♠, 8♠, 5♣]
Discard:  [4♠, Q♥]

 1 Cards = [5♣, 6♣, 8♠, K♠], Cut = A♥ - Points = 4
 2 Cards = [5♣, 6♣, 8♠, K♠], Cut = A♣ - Points = 4
 3 Cards = [5♣, 6♣, 8♠, K♠], Cut = A♠ - Points = 4
 4 Cards = [5♣, 6♣, 8♠, K♠], Cut = A♦ - Points = 4
 5 Cards = [5♣, 6♣, 8♠, K♠], Cut = 2♥ - Points = 4
 6 Cards = [5♣, 6♣, 8♠, K♠], Cut = 2♠ - Points = 4
 7 Cards = [5♣, 6♣, 8♠, K♠], Cut = 2♣ - Points = 4
 8 Cards = [5♣, 6♣, 8♠, K♠], Cut = 2♦ - Points = 4
 9 Cards = [5♣, 6♣, 8♠, K♠], Cut = 3♥ - Points = 2
10 Cards = [5♣, 6♣, 8♠, K♠], Cut = 3♦ - Points = 2
11 Cards = [5♣, 6♣, 8♠, K♠], Cut = 3♣ - Points = 2
12 Cards = [5♣, 6♣, 8♠, K♠], Cut = 3♠ - Points = 2
13 Cards = [5♣, 6♣, 8♠, K♠], Cut = 4♣ - Points = 7
14 Cards = [5♣, 6♣, 8♠, K♠], Cut = 4♥ - Points = 7
15 Cards = [5♣, 6♣, 8♠, K♠], Cut = 4♦ - Points = 7
16 Cards = [5♣, 6♣, 8♠, K♠], Cut = 5♦ - Points = 6
17 Cards = [5♣, 6♣, 8♠, K♠], Cut = 5♥ - Points = 6
18 Cards = [5♣, 6♣, 8♠, K♠], Cut = 5♠ - Points = 6
19 Cards = [5♣, 6♣, 8♠, K♠], Cut = 6♦ - Points = 4
20 Cards = [5♣, 6♣, 8♠, K♠], Cut = 6♥ - Points = 4
21 Cards = [5♣, 6♣, 8♠, K♠], Cut = 6♠ - Points = 4
22 Cards = [5♣, 6♣, 8♠, K♠], Cut = 7♥ - Points = 8
23 Cards = [5♣, 6♣, 8♠, K♠], Cut = 7♠ - Points = 8
24 Cards = [5♣, 6♣, 8♠, K♠], Cut = 7♣ - Points = 8
25 Cards = [5♣, 6♣, 8♠, K♠], Cut = 7♦ - Points = 8
26 Cards = [5♣, 6♣, 8♠, K♠], Cut = 8♣ - Points = 4
27 Cards = [5♣, 6♣, 8♠, K♠], Cut = 8♦ - Points = 4
28 Cards = [5♣, 6♣, 8♠, K♠], Cut = 8♥ - Points = 4
29 Cards = [5♣, 6♣, 8♠, K♠], Cut = 9♥ - Points = 4
30 Cards = [5♣, 6♣, 8♠, K♠], Cut = 9♦ - Points = 4
31 Cards = [5♣, 6♣, 8♠, K♠], Cut = 9♣ - Points = 4
32 Cards = [5♣, 6♣, 8♠, K♠], Cut = 9♠ - Points = 4
33 Cards = [5♣, 6♣, 8♠, K♠], Cut = T♦ - Points = 4
34 Cards = [5♣, 6♣, 8♠, K♠], Cut = T♥ - Points = 4
35 Cards = [5♣, 6♣, 8♠, K♠], Cut = T♠ - Points = 4
36 Cards = [5♣, 6♣, 8♠, K♠], Cut = T♣ - Points = 4
37 Cards = [5♣, 6♣, 8♠, K♠], Cut = J♥ - Points = 4
38 Cards = [5♣, 6♣, 8♠, K♠], Cut = J♠ - Points = 4
39 Cards = [5♣, 6♣, 8♠, K♠], Cut = J♦ - Points = 4
40 Cards = [5♣, 6♣, 8♠, K♠], Cut = J♣ - Points = 4
41 Cards = [5♣, 6♣, 8♠, K♠], Cut = Q♠ - Points = 4
42 Cards = [5♣, 6♣, 8♠, K♠], Cut = Q♦ - Points = 4
43 Cards = [5♣, 6♣, 8♠, K♠], Cut = Q♣ - Points = 4
44 Cards = [5♣, 6♣, 8♠, K♠], Cut = K♣ - Points = 6
45 Cards = [5♣, 6♣, 8♠, K♠], Cut = K♦ - Points = 6
46 Cards = [5♣, 6♣, 8♠, K♠], Cut = K♥ - Points = 6

Average hand value = 4.630434782608695
hand = Hand([Card('3', 'C'), Card('4', 'D'), Card('5', 'H'), Card('5', 'C')])
discard = Hand([Card('2','C'), Card('J', 'C')])

deck = make_deck()

for c in hand:
    deck.remove(c)
    
for c in discard:
    deck.remove(c)
    
print('Candidate Hand: ',hand)
print('Discard: ', discard)
print()

total = 0.0
for i, cut in enumerate(sorted(deck), 1):
    scores, counts = score_hand(hand, cut)    
    value = sum(scores.values())
    print('{:>2} Cards = {}, Cut = {} - Points = {}'.format(i, hand.sorted(), cut, value))
    total += value
    
    
print()
print('Average hand value = {:.3f}'.format(total/len(deck)))

Output:

Candidate Hand:  [3♣, 4♦, 5♥, 5♣]
Discard:  [2♣, J♣]

 1 Cards = [3♣, 4♦, 5♥, 5♣], Cut = A♦ - Points = 10
 2 Cards = [3♣, 4♦, 5♥, 5♣], Cut = A♥ - Points = 10
 3 Cards = [3♣, 4♦, 5♥, 5♣], Cut = A♣ - Points = 10
 4 Cards = [3♣, 4♦, 5♥, 5♣], Cut = A♠ - Points = 10
 5 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 2♦ - Points = 12
 6 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 2♥ - Points = 12
 7 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 2♠ - Points = 12
 8 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 3♦ - Points = 20
 9 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 3♥ - Points = 20
10 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 3♠ - Points = 20
11 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 4♥ - Points = 16
12 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 4♣ - Points = 16
13 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 4♠ - Points = 16
14 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 5♦ - Points = 17
15 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 5♠ - Points = 17
16 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 6♦ - Points = 14
17 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 6♥ - Points = 14
18 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 6♣ - Points = 14
19 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 6♠ - Points = 14
20 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 7♦ - Points = 12
21 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 7♥ - Points = 12
22 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 7♣ - Points = 12
23 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 7♠ - Points = 12
24 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 8♦ - Points = 10
25 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 8♥ - Points = 10
26 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 8♣ - Points = 10
27 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 8♠ - Points = 10
28 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 9♦ - Points = 8
29 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 9♥ - Points = 8
30 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 9♣ - Points = 8
31 Cards = [3♣, 4♦, 5♥, 5♣], Cut = 9♠ - Points = 8
32 Cards = [3♣, 4♦, 5♥, 5♣], Cut = T♦ - Points = 12
33 Cards = [3♣, 4♦, 5♥, 5♣], Cut = T♥ - Points = 12
34 Cards = [3♣, 4♦, 5♥, 5♣], Cut = T♣ - Points = 12
35 Cards = [3♣, 4♦, 5♥, 5♣], Cut = T♠ - Points = 12
36 Cards = [3♣, 4♦, 5♥, 5♣], Cut = J♦ - Points = 12
37 Cards = [3♣, 4♦, 5♥, 5♣], Cut = J♥ - Points = 12
38 Cards = [3♣, 4♦, 5♥, 5♣], Cut = J♠ - Points = 12
39 Cards = [3♣, 4♦, 5♥, 5♣], Cut = Q♦ - Points = 12
40 Cards = [3♣, 4♦, 5♥, 5♣], Cut = Q♥ - Points = 12
41 Cards = [3♣, 4♦, 5♥, 5♣], Cut = Q♣ - Points = 12
42 Cards = [3♣, 4♦, 5♥, 5♣], Cut = Q♠ - Points = 12
43 Cards = [3♣, 4♦, 5♥, 5♣], Cut = K♦ - Points = 12
44 Cards = [3♣, 4♦, 5♥, 5♣], Cut = K♥ - Points = 12
45 Cards = [3♣, 4♦, 5♥, 5♣], Cut = K♣ - Points = 12
46 Cards = [3♣, 4♦, 5♥, 5♣], Cut = K♠ - Points = 12

Average hand value = 12.478

Average Hand Method

def average_hand_value(deck, hand, **kwargs):
    """
    The player is dealt 6 cards which are removed from 
    the deck. This method calculates the average hand value, 
    given the remaining cards in the deck (46) and a hand of 4 cards.
    
    Essentially, it uses the remaining cards in the deck, each one in turn, 
    as the potential starter card. The hand value + the starter card is 
    calculated. The average value is returned
    
    Parameters
    ----------
    deck - iterable - a list of cards that are remaining in the deck. There should be 46.
    hand - Hand object - a list of cards that the player wants to evaluate. There should be 4 cards.
    
    Returns
    -------
    
    The average hand value.
    
    """
   
    assert len(deck) == 46
    assert len(hand) == 4
    
    verbose = False if 'verbose' not in kwargs else kwargs['verbose']
    
    total = 0.0
    for i, cut in enumerate(sorted(deck), 1):
        scores, counts = score_hand(hand, cut)    
        value = sum(scores.values())
        total += value
        
        if verbose:
            print('{:>2} Cards = {}, Cut = {} - Points = {}'.format(i, hand.sorted(), cut, value))
        
    return total/len(deck)  
hand = Hand([Card('3', 'C'), Card('4', 'D'), Card('5', 'H'), Card('5', 'C')])
discard = Hand([Card('2','C'), Card('J', 'C')])

deck = make_deck()

for c in hand:
    deck.remove(c)
    
for c in discard:
    deck.remove(c)
    
print('Candidate Hand: ',hand)
print('Discard: ', discard)
print()

average = average_hand_value(deck, hand)

print('Average hand value = {:.3f}'.format(average))

Output:

Candidate Hand:  [3♣, 4♦, 5♥, 5♣]
Discard:  [2♣, J♣]

Average hand value = 12.478

Find the Best Discard

Given 6 cards and the starter card is not turned up, find the best set of 4 cards to keep. That is the 4 cards with the highest point total and the highest hand average

def determine_best_hand(hand, **kwargs):
    """
    Takes a hand of 6 cards and determines the best 4 hand combination based on point value.
    """    
    assert len(hand) == 6
    
    verbose = False if 'verbose' not in kwargs else kwargs['verbose']
    
    max_score = 0.0
    max_average = 0.0
    best_hand = None
    
    for combo in hand.every_combination(count=4):
        new_hand = Hand(combo).sorted()        
        scores, counts = score_hand(new_hand, None) # we are ignoring the cut card when scoring|
        score = sum(scores.values())
                
        deck = make_deck()

        for c in hand:
            deck.remove(c)

        average = average_hand_value(deck, new_hand)
        
        if verbose:
            print('Hand = {}, value = {}, average = {:.4f}'.format(new_hand, score, average))
                    
        if score >= max_score and average > max_average:
            max_score = score
            max_average = average
            best_hand = new_hand
    
    return max_score, best_hand
deck = make_deck()

# shuffle the deck
random.shuffle(deck)

# extract the first 6 cards (removing them from the deck)
hand = make_hand(deck, 6)
print('Candidate Hand = {} '.format(hand))
print('-------')

score, best_hand = determine_best_hand(hand,verbose=True)
print('---------')
print('Best Hand      = {}'.format(best_hand))

scores, counts = score_hand(best_hand, None)
print('Hand Value     = {}'.format(sum(scores.values())))

# find the average hand value
average = average_hand_value(deck, best_hand)

print()
print('Average hand value = {:.4f}'.format(average))   

Output:

Candidate Hand = [K♥, 7♦, 9♦, A♦, 8♣, J♦] 
-------
Hand = [A♦, 7♦, 9♦, K♥], value = 0, average = 1.6739
Hand = [7♦, 8♣, 9♦, K♥], value = 5, average = 6.8913
Hand = [7♦, 9♦, J♦, K♥], value = 0, average = 2.0870
Hand = [A♦, 7♦, 8♣, K♥], value = 2, average = 3.8913
Hand = [A♦, 7♦, J♦, K♥], value = 0, average = 1.9348
Hand = [7♦, 8♣, J♦, K♥], value = 2, average = 4.0435
Hand = [A♦, 8♣, 9♦, K♥], value = 0, average = 1.9783
Hand = [A♦, 9♦, J♦, K♥], value = 0, average = 2.2826
Hand = [8♣, 9♦, J♦, K♥], value = 0, average = 2.1739
Hand = [A♦, 8♣, J♦, K♥], value = 0, average = 1.9783
Hand = [A♦, 7♦, 8♣, 9♦], value = 5, average = 7.1957
Hand = [A♦, 7♦, 9♦, J♦], value = 4, average = 6.3261
Hand = [7♦, 8♣, 9♦, J♦], value = 5, average = 7.1739
Hand = [A♦, 7♦, 8♣, J♦], value = 2, average = 4.0870
Hand = [A♦, 8♣, 9♦, J♦], value = 0, average = 2.2609
---------
Best Hand      = [A♦, 7♦, 8♣, 9♦]
Hand Value     = 5

Average hand value = 7.1957

A lot of the literature doesn't flushes or nobs or other things. This method uses the proper cribbage scoring and doesn't make any simplifications.

Analyze Average Discard Value

From the literature, only discard tables are available. There is really no detail in how the tables were calculated or the assumptions used when creating them.

Following the approach from the previous section we need to:

  1. Take the cards to discard and pair them with two other cards from the remaining cards
  2. Take a starter from the deck
  3. score the crib along with the starter and determine its average value
  4. Search for the maximum value and the minimum value
def average_crib_value(deck, discard, **kwargs):
    """
    The player is dealt 6 cards which are removed from 
    the deck. They have picked 2 cards to discard to the crib.    
    This method calculates the average crib value, 
    given the remaining cards in the deck (46) and 2 discard cards
    intended for the crib.
    
    Essentially, it uses the remaining cards in the deck, each pair in turn, 
    as the potential crib mates and an extra card for the starter. 
    
    Parameters
    ----------
    deck - iterable - a list of cards that are remaining in the deck. There should be 46.
    discard - Hand object - a list of cards that will be part of the crib. There should be 2 cards.
    
    Returns
    -------
    
    The average crib value.
    
    """
   
#     assert len(deck) == 46
    assert len(discard) == 2
    
    verbose = False if 'verbose' not in kwargs else kwargs['verbose']
    
    total = 0.0 
    count = 0
        
    for i, combo in enumerate(combinations(deck, 3), 1):
        c1, c2, cut = combo
        crib = Hand([c1, c2])
        crib.extend(discard)
        
        scores, counts = score_hand(crib, cut, is_crib=True)    
        value = sum(scores.values())
        total += value
        count += 1
        
        if verbose:
            print('{:>2} Crib = {} Cut = {} Points = {}'.format(i, crib.sorted(), cut, value))
        
    average = total/count
        
    return average
hand = Hand([Card('4', 'C'),  Card('Q', 'C'), Card('A','C'), Card('A', 'D')])
discard = Hand([Card('5', 'D'), Card('Q', 'H')])

deck = make_deck()

for c in hand:
    deck.remove(c)

for c in discard:
    deck.remove(c)
    
print('Candidate Hand = {} '.format(hand.sorted()))
print('Discard: ', discard.sorted())
print('-------')

# find the average hand value
average = average_crib_value(deck, discard, verbose=False)

print()
print('Average crib value = {:.4f}'.format(average))        
print('-------')
print()

Output:

Candidate Hand = [A♣, A♦, 4♣, Q♣] 
Discard:  [5♦, Q♥]
-------

Average crib value = 6.9283

Construct Crib Discard Table

On my computer it takes about 6 seconds to compute the average value of the discard to the crib. To speed things up we'll create a lookup dictionary contianing all possible pairings of the cards and the average value. Once the dictionary has been calculated we'll store it as .json so we don't need to do it every time.

I don't think counting flushes will make much of a difference to this process. To save computation time, I'll reduce all duplicate ranks from the deck

deck = make_deck()
candidates = sorted(deck)[0::2]

average_crib_values = {}

# attempt to load the pre-calculated averages
try:
    
    with open('crib_discard.json', 'r') as fp:
        average_crib_values = json.load(fp)
    
except FileNotFoundError:
    # there is no pre-calculated dictionary
    pass
    
for i, combo in enumerate(combinations(candidates, 2), 1):
        discard = Hand(combo)
        key = str([c.rank for c in discard])
        
        if key not in average_crib_values:        
            reduced_deck = list(set(deck) - set(discard))
            average = average_crib_value(reduced_deck, discard, verbose=False)
            average_crib_values[key] = average
            print('{:<2} Discard = {} Average = {:.4f}'.format(i, key, average_crib_values[key]))            
# dump the averages into a json file so we don't have to recalculate it every time
with open('crib_discard.json', 'w') as fp:
    json.dump(average_crib_values, fp, indent=4)
    
print('Overall average crib points = {:.4f}'.format(sum(average_crib_values.values())/len(average_crib_values)))
Overall average crib points = 4.8762

Expected Average

For the dealer the expected average is the Average Hand Value + the Average Discard Value

For the pone the expected average is the Average Hand Value - the Average Discard Value

hand = Hand([Card('4', 'C'),  Card('Q', 'C'), Card('A','C'), Card('A', 'D'), Card('5', 'D'), Card('Q', 'H')])
deck = make_deck()

for c in hand:
    deck.remove(c)

As Dealer

As the dealer, we want to maximize the amount of points we have in our hand and in the crib. I think the best approach is to maximize the value of the hand as the crib, on average is only valued at about 4.88 points.

print('Candidate Hand = {} '.format(hand.sorted()))
print('')

score, best_hand = determine_best_hand(hand,verbose=False)
print('---------')
print('Best Hand      = {}'.format(best_hand))
print('Hand Value     = {}'.format(score))

# find the average hand value
hand_average = average_hand_value(deck, best_hand)
print('Average hand value = {:.4f}'.format(hand_average))  

discard = Hand(list(set(hand) - set(best_hand)))
print('Discard: ', discard.sorted())
print('-------')

# find the average crib value. This takes into account flushes and all suits. 
# No simplification is made, the calculations are correct as far as I can tell.
average_crib = average_crib_value(deck, discard, verbose=False)

# in the cached dictionary of pre-calculated values suits where basically ignored 
# so the values will be slightly different then the other method
# key = str([c.rank for c in discard])
key = str(sorted([c.rank for c in discard]))
average_crib_precalc = average_crib_values[key]

print()
print('Average crib value                  = {:.4f}'.format(average_crib))        
print('Average crib value (pre-calculated) = {:.4f}'.format(average_crib_precalc))        
print('-------')
print()

print('Expected Average - Dealer = {}'.format(hand_average + average_crib))
print('Expected Average - Dealer = {}'.format(hand_average + average_crib_precalc))
Candidate Hand = [A♣, A♦, 4♣, 5♦, Q♣, Q♥] 

---------
Best Hand      = [A♣, A♦, 4♣, Q♣]
Hand Value     = 6
Average hand value = 8.4348
Discard:  [5♦, Q♥]
-------

Average crib value                  = 6.9283
Average crib value (pre-calculated) = 6.9634
-------

Expected Average - Dealer = 15.36304347826087
Expected Average - Dealer = 15.398149955634427

As Pone

The strategy for the pone is different. They want to maximize the average value of their hand while at the same time minimize the average value of the crib. This will take a little different strategy.

print('Candidate Hand = {} '.format(hand.sorted()))
print('')

score, best_hand = determine_best_hand(hand,verbose=False)
print('---------')
print('Best Hand      = {}'.format(best_hand))
print('Hand Value     = {}'.format(score))

# find the average hand value
hand_average = average_hand_value(deck, best_hand)
print('Average hand value = {:.4f}'.format(hand_average))  

discard = Hand(list(set(hand) - set(best_hand)))
print('Discard: ', discard.sorted())
print('-------')

# find the average crib value. This takes into account flushes and all suits. 
# No simplification is made, the calculations are correct as far as I can tell.
average_crib = average_crib_value(deck, discard, verbose=False)

# in the cached dictionary of pre-calculated values suits where basically ignored 
# so the values will be slightly different then the other method
# key = str([c.rank for c in discard])
key = str(sorted([c.rank for c in discard]))
average_crib_precalc = average_crib_values[key]

print()
print('Average crib value                  = {:.4f}'.format(average_crib))        
print('Average crib value (pre-calculated) = {:.4f}'.format(average_crib_precalc))        
print('-------')
print()

print('Expected Average - Pone = {}'.format(hand_average - average_crib))
print('Expected Average - Pone = {}'.format(hand_average - average_crib_precalc))
Candidate Hand = [A♣, A♦, 4♣, 5♦, Q♣, Q♥] 

---------
Best Hand      = [A♣, A♦, 4♣, Q♣]
Hand Value     = 6
Average hand value = 8.4348
Discard:  [5♦, Q♥]
-------

Average crib value                  = 6.9283
Average crib value (pre-calculated) = 6.9634
-------

Expected Average - Pone = 1.5065217391304353
Expected Average - Pone = 1.471415261756877

Maximizing the value of the pone's hand doesn't yield an optimal strategy for the pone overall. In the above case it looks as though the pone can only expect about 1.5 points on average even though the hand value is 8.4! We need a different strategy to minimize the loss in points.

def four_card_hand_scores(hand, **kwargs):
    """
    Takes a hand of more than 4 cards and returns a list containing 
    every 4 card hand combination along with the point value, point value dictionary and
    a counts dictionary outlining the number of matches for the point value dictionary.
            
    """
    hands = []
    
    for combo in hand.every_combination(count=4):
        new_hand = Hand(combo).sorted()        
        
        # we are only dealing with 4 cards, ignoring a cut card
        scores, counts = score_hand(new_hand, None) 
        score = sum(scores.values())
        
        hands.append((new_hand, score, scores, counts))
    
    return hands
def average_hand_values(deck, hands, **kwargs):
    """
    Calculates the average value for the hands.
    
    Parameters
    ----------
    deck - the cards left unturned as far as the person (pone or dealer) 
           that are used to calculate the average hand value. 
    
    hands - tuple(Hand, number, dictionary, dictionary) - the list of hands to evaluate 
            the average values for.
            
    Returns
    -------
    a list of tuples similar to the input list of tuples that contain the hand,
    the value of the hand and the average value.
    
    """
    
    assert len(deck) == 46

    average_hands = []
    for hand_tuple in hands:
        hand, hand_value, *_ = hand_tuple
        average_value = average_hand_value(deck, hand)
    
        average_hands.append((hand, hand_value, average_value))
    
    return average_hands
def average_discard_values(hand, average_hands, **kwargs):
    
    average_discards = []
    for i, a in enumerate(average_hands):
        average_hand, value, average = a
        discard = list(set(hand) - set(average_hand))
    #     average_crib = average_crib_value(deck, discard, verbose=False)

        try:

            key = str(sorted([c.rank for c in discard]))
            average_crib_precalc = average_crib_values[key]

        except KeyError:        
            key = str(sorted([c.rank for c in discard], reverse=True))
            average_crib_precalc = average_crib_values[key]

        average_discards.append((average_hand, 
                                 value, 
                                 average, 
                                 discard,
                                 average_crib_precalc))
        
    return average_discards
hands = sorted(four_card_hand_scores(hand), key=lambda x:x[1], reverse=True)
average_hands = sorted(average_hand_values(deck, hands), key=lambda x:x[2], reverse=True)
average_discards = sorted(average_discard_values(hand, average_hands), key=lambda x:x[4])
   
row = '{:<3} {} = {} -> Average = {:.4f} - Discard = {} -> Average = {:.4f} -> EA = {:.4f}'
for i, ad in enumerate(average_discards):    
    print(row.format(i,*ad, ad[2] - ad[4]))
0   [A♣, 4♣, 5♦, Q♣] = 4 -> Average = 7.0000 - Discard = [A♦, Q♥] -> Average = 3.8951 -> EA = 3.1049
1   [A♦, 4♣, 5♦, Q♣] = 4 -> Average = 7.0000 - Discard = [A♣, Q♥] -> Average = 3.8951 -> EA = 3.1049
2   [A♣, 4♣, 5♦, Q♥] = 4 -> Average = 7.0000 - Discard = [A♦, Q♣] -> Average = 3.8951 -> EA = 3.1049
3   [A♦, 4♣, 5♦, Q♥] = 4 -> Average = 7.0000 - Discard = [A♣, Q♣] -> Average = 3.8951 -> EA = 3.1049
4   [A♣, A♦, 5♦, Q♣] = 4 -> Average = 6.0870 - Discard = [4♣, Q♥] -> Average = 4.0782 -> EA = 2.0087
5   [A♣, A♦, 5♦, Q♥] = 4 -> Average = 6.0870 - Discard = [4♣, Q♣] -> Average = 4.0782 -> EA = 2.0087
6   [A♣, A♦, 4♣, 5♦] = 2 -> Average = 6.0435 - Discard = [Q♥, Q♣] -> Average = 5.2502 -> EA = 0.7933
7   [A♣, 5♦, Q♣, Q♥] = 6 -> Average = 7.6957 - Discard = [4♣, A♦] -> Average = 5.5085 -> EA = 2.1871
8   [A♦, 5♦, Q♣, Q♥] = 6 -> Average = 7.6957 - Discard = [4♣, A♣] -> Average = 5.5085 -> EA = 2.1871
9   [4♣, 5♦, Q♣, Q♥] = 6 -> Average = 8.1739 - Discard = [A♦, A♣] -> Average = 5.5335 -> EA = 2.6404
10  [A♣, 4♣, Q♣, Q♥] = 6 -> Average = 7.6957 - Discard = [A♦, 5♦] -> Average = 5.7722 -> EA = 1.9234
11  [A♦, 4♣, Q♣, Q♥] = 6 -> Average = 7.6957 - Discard = [A♣, 5♦] -> Average = 5.7722 -> EA = 1.9234
12  [A♣, A♦, 4♣, Q♣] = 6 -> Average = 8.4348 - Discard = [Q♥, 5♦] -> Average = 6.9634 -> EA = 1.4714
13  [A♣, A♦, 4♣, Q♥] = 6 -> Average = 8.4348 - Discard = [5♦, Q♣] -> Average = 6.9634 -> EA = 1.4714
14  [A♣, A♦, Q♣, Q♥] = 4 -> Average = 5.4783 - Discard = [4♣, 5♦] -> Average = 7.0352 -> EA = -1.5569

By minimizing the average points discarded to the crib, we maximize our expected average points for the hand.

Troy Williams

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