In [242]:
from enum import Enum
from typing import List, Tuple
from collections import Counter, OrderedDict
from itertools import combinations, product
import operator

In [183]:
DICE_PER_ROLL = 5
DICE_VALUES = [1,2,3,4,5,6]
INDENT = 22

In [184]:
def sum_of_single_value(input_list: Tuple[int], value: int):
    return input_list.count(value) * value

In [185]:
def n_of_a_kind(input_list: Tuple[int], n: int):
    for value, count in Counter(input_list).items():
        if count >= n:
            if n == DICE_PER_ROLL:
                return 50
            else:
                return sum(input_list)
    return 0

In [186]:
def full_house(input_list: Tuple[int]):
    if set(Counter(input_list).values()) == {3, 2}:
        return 25
    else:
        return 0

In [187]:
def find_longest_sequence(input_list: Tuple[int]):
    max_seq = 1
    nums = set(input_list)
    for num in nums:
        this_seq = 1
        seq = True
        while seq:
            if num + this_seq in nums:
                this_seq += 1
            else:
                seq = False
        
        max_seq = max(this_seq, max_seq)
    
    return max_seq

In [188]:
def straight(input_list: Tuple[int], size: int):
    seq = find_longest_sequence(input_list)
    
    if seq >= size:
        if size == DICE_PER_ROLL:
            return 40
        elif size == DICE_PER_ROLL - 1:
            return 30
        
    return 0

In [244]:
hand_scoring_functions = [
    lambda x: sum_of_single_value(x, 1),
    lambda x: sum_of_single_value(x, 2),
    lambda x: sum_of_single_value(x, 3),
    lambda x: sum_of_single_value(x, 4),
    lambda x: sum_of_single_value(x, 5),
    lambda x: sum_of_single_value(x, 6),
    lambda x: n_of_a_kind(x, 3),
    lambda x: n_of_a_kind(x, 4),
    lambda x: full_house(x),
    lambda x: straight(x, 4),
    lambda x: straight(x, 5),
    lambda x: n_of_a_kind(x, 5),
    lambda x: sum(x)
]

hand_scoring_names = [
    "ONES",
    "TWOS",
    "THREES",
    "FOURS",
    "FIVES",
    "SIXES",
    "THREE OF A KIND",
    "FOUR OF A KIND",
    "FULL HOUSE",
    "SMALL STRAIGHT",
    "LARGE STRAIGHT",
    "YAHTZEE",
    "CHANCE"
]

In [190]:
def calculate_hand_scores(roll: Tuple[int], debug: bool = False):
    hand_scores = [func(roll) for func in hand_scoring_functions]
    if debug:
        print(f'{"all hand scores:":>{INDENT}} {hand_scores}')
    return hand_scores

In [247]:
def calc_max_score(roll: Tuple[int], debug: bool = False):
    max_index, max_value = max(enumerate(calculate_hand_scores(roll, debug=debug)[::-1]),
                               key=operator.itemgetter(1))
    
    if debug:
        print(f'{"max roll score:":>{INDENT}} {max_value}')
        print(f'{"catgeory:":>{INDENT}} {hand_scoring_names[::-1][max_index]}')
    return max_value

In [235]:
def all_possible_final_hands(saved_dice: Tuple[int], debug: bool = False):
    number_of_rerolls = DICE_PER_ROLL - len(saved_dice)
    possible_reroll_outcomes = []
    
    for reroll in product(DICE_VALUES, repeat=number_of_rerolls):
        hand = tuple(saved_dice + tuple(sorted(reroll)))
        possible_reroll_outcomes.append(hand)
    
    return Counter(possible_reroll_outcomes)

In [258]:
def calculate_expected_score(saved_dice: Tuple[int], debug: bool = False):
    final_rolls = all_possible_final_hands(saved_dice, debug=debug)
    number_of_final_hands = sum(final_rolls.values())
    running_expected_score = 0
    running_probability = 0
    
    if debug:
        print(f'{"total rerolled hands:":>{INDENT}} {number_of_final_hands}')
        print()
    
    for roll, count in final_rolls.items():
        if debug:
            print(f'{"possible roll:":>{INDENT}} {roll}')
            print(f'{"count:":>{INDENT}} {count}')
        this_max_score = calc_max_score(roll, debug=debug)
        chance_of_roll = (count/number_of_final_hands)
        expected_score = this_max_score * chance_of_roll
                                        
        if debug:
            print(f'{"chance of roll :":>{INDENT}} {chance_of_roll*100:.2f}')
            print(f'{"expected contrib :":>{INDENT}} {chance_of_roll*100:.2f}')
            print()
        running_expected_score += expected_score
        running_probability += chance_of_roll
    
    if debug:
        print(f'{"total expected :":>{INDENT}} {running_expected_score:.2f}')
        print(f'{"sum of chances :":>{INDENT}} {running_probability*100:.2f}')
        
    return running_expected_score

In [252]:
def calculate_all_save_possibilities(input_list: Tuple[int], debug: bool = False):
    saved_dice = {tuple(), }
    for i in range(1, len(input_list)+1):
        for combo in combinations(input_list, i):
            saved_dice.add(tuple(sorted(combo)))
            
    return saved_dice

In [298]:
def simulate(second_to_last_roll: Tuple[int], debug: bool = False):
    result = dict()
    
    for saved_dice in calculate_all_save_possibilities(second_to_last_roll, debug=debug):
        if debug: print(f'{"saved dice:":>{INDENT}} {saved_dice}')
        result[saved_dice] = calculate_expected_score(saved_dice, debug=debug)
        if debug: print()
    
    
        
    #print(f'{"saved_dice":<18}score')
    #for (saved_roll, expected) in sorted(result.items(), key=lambda x: x[1]):
    #    print(f'{str(saved_roll):<18}{expected:.2f}')
        
    return {k:v for k,v in sorted(result.items(), key=lambda x: -x[1])}

In [299]:
results = simulate((4,4,4,5,5), debug=False)

In [300]:
from matplotlib import pyplot as plt

In [346]:
def plot_reroll_expecteds(results):
    fig, ax = plt.subplots(figsize=(10,6))
    bar_locs = list(range(len(results.values())))
    ax.barh(bar_locs,
            results.values(),
            tick_label=[' '.join(map(str,k)) for k in results.keys()],
            height=1,
           edgecolor='k')
    ax.set_title('expected score for combinations of saved dice')
    ax.set_xlabel('expected score')
    ax.set_ylabel('dice saved after second roll')
    ax.set_ylim(min(bar_locs)-0.5, max(bar_locs)+0.5)
    #ax.set_xlim(18, 26)
    plt.show()

In [347]:
plot_reroll_expecteds(results)



In [ ]: