Example #1
0
class YahtzeeRoll:
    ''' The outcome of rolling 0 through 5 6-sided dice.
    '''

    def __init__(self):
        ''' Creates an empty roll.
        '''
        self.dice = Multiset(6)

    def reroll(self):
        ''' Adds dice showing uniformly randomly selected numbers to this roll until
            this roll has 5 dice.
        '''
        self.dice.add_random(5 - self.dice.size())


    def subroll(self, other):
        ''' Determines if this roll is a subset of the given roll.

            other -- a Yahtzee roll
        '''
        return self.dice.subset(other.dice)


    def count(self, num):
        ''' Determines how mnay dice are showing the given number.

            num -- an integer
        '''
        return self.dice.count(num - 1)


    def total(self):
        ''' Returns the total showing on the dice.
        '''
        # total in the dice + 1 for each to account for 0...5 vs 1...6
        return self.dice.total() + self.dice.size()


    def is_n_kind(self, n):
        ''' Determines if this roll is n-of-a-kind.

            n -- a positive integer
        '''
        i = 1
        while i <= 6 and self.count(i) < n:
            i += 1
        return i <= 6


    def is_full_house(self):
        ''' Determines if this roll is a full house.
        '''
        double = False
        triple = False
        i = 1
        while i <= 6 and not (double and triple) and self.count(i) in [0, 2, 3]:
            if self.count(i) == 2:
                double = True
            elif self.count(i) == 3:
                triple = True
            i += 1
        return double and triple


    def is_straight(self, n):
        ''' Determines if this roll is a straight of at least the given length.

            n -- a positive integer
        '''
        consec = 0
        i = 1
        while i <= 6 and consec < n:
            if self.count(i) > 0:
                consec += 1
            else:
                consec = 0
            i += 1
        return consec == n


    @staticmethod
    def parse(str):
        ''' Returns a roll containing dice showing the numbers corresponding to the
            digits in the given string.

            str -- a string containing up to 5 digits, each in the range 1 through 6
        '''
        if len(str) > 5:
            raise ValueError("length must be at most 5: " + str)

        result = YahtzeeRoll()
        for digit in str:
            if not digit.isdigit():
                raise ValueError("invalid digit in " + str)
            num = int(digit)
            if num < 1 or num > 6:
                raise ValueError("invalid digit in " + str)
            result.dice.add(num - 1)
        return result


    def as_list(self):
        ''' Returns a list of the numbers showing in this roll.'''
        return [x + 1 for x in self.dice.as_list()]


    def select_all(self, nums, maximum=5):
        ''' Returns the subroll of this roll that contains all occurrences
            of the given numbers.

            nums -- a list of integers betwen 1 and 6
        '''
        keep = ""
        for n in nums:
            if n < 1 or n > 6:
                raise ValueError("value out of range in " + str(nums))
            for i in range(min(self.count(n), maximum)):
                keep = keep + str(n)
        return YahtzeeRoll.parse(keep)


    def select_one(self, nums):
        ''' Returns the subroll of this roll that contains one occurrence
            of each of the given numbers that are also in this roll.

            nums -- a list of integers betwen 1 and 6
        '''
        keep = ""
        for n in nums:
            if n < 1 or n > 6:
                raise ValueError("value out of range in " + str(nums))
            if self.count(n) > 0:
                keep = keep + str(n)
        return YahtzeeRoll.parse(keep)


    def select_for_chance(self, rerolls):
        ''' Returns the subroll of this roll that maximizes the expected
            score in chance.

            rerolls -- 1 or 2
        '''
        if rerolls == 2:
            return self.select_all([5, 6])
        else:
            return self.select_all([4, 5, 6])


    def select_for_full_house(self):
        ''' Returns a subroll of this roll that gives the chance of
            obtaining a full house.
        '''
        # keep up to three of numbers we have at least 2 of
        keep = []
        for i in range(1, 7):
            if self.count(i) >= 2:
                keep.append(i)
        return self.select_all(keep, 3)


    def select_for_straight(self, sheet):
        ''' Returns a subroll that gives a good chance of obtaining
            the longest straight left unmarked on the given scoresheet.

            sheet -- a Yahtzee scoresheet
        '''
        # if SS is open, keep longest run
        if not sheet.is_marked(YahtzeeScoresheet.SMALL_STRAIGHT):
            runs = self.longest_runs()
            if len(runs[0]) >= 3:
                return self.select_one(runs[0])
            else:
                # choose between possibly multiple runs of 2; keep the higher
                # one if chance is open or it has strictly more open categories
                counts = [sum([(0 if sheet.is_marked(n - 1) else 1) for n in x]) for x in runs]
                run = runs[0]
                if len(runs) > 1 and (not sheet.is_marked(YahtzeeScoresheet.CHANCE) or counts[1] > counts[0]):
                    run = runs[1]
                return self.select_one(run)
        else:
            # keep the straight that we have the most of
            low = self.select_one(range(1, 6))
            high = self.select_one(range(2, 7))

            if len(low.as_list()) > len(high.as_list()):
                return low
            else:
                return high

#12346

    def longest_runs(self):
        ''' Returns a list of all the longest consecutive runs in this
            roll.  For example, if this roll is [1 2 4 4 5] then the
            list returned is [[1, 2], [4, 5]].
        '''
        runs = []
        longest = 0
        curr_len = 0
        for i in range(1, 7):
            if self.count(i) > 0:
                curr_len += 1
                if curr_len == longest:
                    runs.append(list(range(i - curr_len + 1, i + 1)))
                elif curr_len > longest:
                    runs = [list(range(i - curr_len + 1, i + 1))]
                    longest = curr_len
            else:
                curr_len = 0
        return runs


    def select_for_n_kind(self, sheet, rerolls):
        ''' Returns the subroll that maximizes expected score in
            3K, 4K, or Y
        '''
        max_keep = 5
        if not sheet.is_marked(YahtzeeScoresheet.FOUR_KIND) and sheet.is_marked(YahtzeeScoresheet.YAHTZEE) and sheet.scores[YahtzeeScoresheet.YAHTZEE] == 0:
            max_keep = 4
        elif not sheet.is_marked(YahtzeeScoresheet.THREE_KIND) and sheet.is_marked(YahtzeeScoresheet.FOUR_KIND) and sheet.is_marked(YahtzeeScoresheet.YAHTZEE) and sheet.scores[YahtzeeScoresheet.YAHTZEE] == 0:
            max_keep = 3
        high_freq = 0
        most_freq = None
        for i in range(1, 7):
            if self.count(i) >= high_freq:
                high_freq = self.count(i)
                most_freq = i

        keep_nums = [most_freq]

        # keep 4's, 5's, and 6's if aready have what we need
        # (4's only if down to last reroll)
        if ((max_keep == 3 and sheet.score(YahtzeeScoresheet.THREE_KIND, self) > 0)
            or (max_keep == 4 and sheet.score(YahtzeeScoresheet.FOUR_KIND, self) > 0)):
            for i in range(3 + rerolls, 7):
                if i != most_freq:
                    keep_nums.append(i)

        return self.select_all(keep_nums, max_keep)


    def __str__(self):
        ''' Returns a string representation of this roll.
        '''
        return str(self.as_list())
Example #2
0
class DiceRoll:
    ''' The outcome of rolling a collection of indistinguishable fair dice.
        Rolls are immutable so they can be used as dictionary keys.
        Includes methods for creating new rolls by adding randomly rolled
        dice to an existing roll, and creating subrolls by selecting
        certain dice from a roll.
    '''

    def __init__(self, pips, sides):
        ''' Creates a roll with dice showing the numbers in the given iterable
            and the given number of sides.

            pips -- an iterable over integers between 1 and sides (inclusive)
            sides -- a positive integer
        '''
        self._dice = Multiset(sides)
        self._hash = None
        for n in pips:
            self._dice.add(n - 1)


    @staticmethod
    def roll(count, sides):
        ''' Creates and returns a random roll of the given number of dice with
            the given number of sides.

            count -- a nonnegative integer
            sides -- a positive integer
        '''
        result = DiceRoll([], sides)
        result._dice.add_random(count)
        return result


    @staticmethod
    def parse(s, sides):
        ''' Returns a roll containing dice showing the numbers corresponding
            to the digits in the given string.

            s -- a string containing digits in the range 1 through sides
            sided -- a positive integer
        '''
        if sides <= 0:
            raise ValueError("number of sides must be positive: {0}".format(sides))
        pips = []
        for digit in s:
            if not digit.isdigit():
                raise ValueError("invalid digit {0} in {1}".format(digit, s))
            num = int(digit)
            if num < 1 or num > sides:
                raise ValueError("invalid digit {0} in {1} for {2} sides".format(digit, s, sides))
            pips.append(num)
        return DiceRoll(pips, sides)


    def size(self):
        ''' Returns the number of dice in this roll.
        '''
        return self._dice.size()


    def sides(self):
        ''' Returns the number of sides on the dice in this roll.
        '''
        return self._dice.maximum()


    def min_number(self):
        ''' Returns the minimum number showing in this roll.
            If there are no dice in this roll then the
            value returned is larger than the number of sides.
        '''
        return self._dice.min_element() + 1


    def max_number(self):
        ''' Returns the maximum number showing in this roll.
            If there are no dice in this roll then the
            value returned is zero or less.
        '''
        return self._dice.max_element() + 1

    
    def copy(self):
        ''' Returns a copy of this roll.
        '''
        return DiceRoll(self.as_list(), self.sides())

            
    def reroll(self, total):
        ''' Creates and returns a roll containing dice showing the same numbers
            as in this roll, plus additional randomly rolled dice so the total
            dice is as given.

            total -- an integer greater than or equal to the number of dice
                     in this roll
        '''
        if total < self.size():
            raise ValueError("can't reroll to fewer dice: {0} < {1}".format(total, self.size()))
        result = self.copy()
        result._dice.add_random(total - self.size())
        return result


    def add_one(self, num):
        ''' Creates and returns a roll containing the same dice as this one
            plus one showing the given number.

            num -- an integer between 1 and the number of sides on the dice
                   in this roll (inclusive)
        '''
        if num < 1 or num > self.sides():
            raise ValueError("number out of range for {1}-sided dice: {0}".format(num, self.sides()))
        result = self.copy()
        result._dice.add(num - 1)
        return result


    def subroll(self, other):
        ''' Determines if this roll is a subset of the given roll.  One roll is
            a subset of another if the dice in the two rolls have the same number
            of sides and there is a 1-1 mapping from the first roll to dice
            in the second showing the same number.
        
            other -- a Yahtzee roll
        '''
        return self.sides() == other.sides() and self._dice.subset(other._dice)


    def count(self, num):
        ''' Determines how mnay dice are showing the given number.
        
            num -- an integer
        '''
        return self._dice.count(num - 1)


    def total(self):
        ''' Returns the total showing on the dice.
        '''
        # total in the dice + 1 for each to account for 0...5 vs 1...6
        return self._dice.total() + self._dice.size()


    def all_subrolls(self):
        ''' Returns a list containing all the subrolls of this roll.
        '''
        result = []

        # for each possible number, compute the range of how may dice
        # showing that number we can keep.  For example, for four-sided
        # dice [1, 2, 2, 4] we want [0..1, 0..2, 0, 0..1]
        options = []
        for i in range(self.sides()):
            options.append(range(self.count(i + 1) + 1))

        # for each possible combination of how many of each number,
        # create the corresponding roll
        for counts in itertools.product(*options):
            s = []
            for i in range(self.sides()):
                # add counts[i] dice showing i + 1
                for k in range(counts[i]):
                    s.append(i + 1)
            result.append(DiceRoll(s, self.sides()))

        return result


    def as_list(self):
        ''' Returns a list of the numbers showing in this roll.  The
            list returned will be sorted from lowest to highest number showing.
        '''
        return [x + 1 for x in self._dice.as_list()]


    def as_tuple(self):
        ''' Returns a tuple of the numbers showing in this roll.  The tuple
            returned will be sorted from lowest to highest number showing.
        '''
        return tuple([x + 1 for x in self._dice.as_list()])


    def select_all(self, nums, maximum=None):
        ''' Returns the subroll of this roll that contains all occurrences
            of the given numbers up to the given maximum of each.  If the
            maximum is None then there is no limit.

            nums -- a list of integers betwen 1 and the number of sides
                    on the dice in this roll
            maximum -- an integer, or None
        '''
        keep = []
        # for each number in the list, add as many of that number as are
        # in the roll, up to the given maximum
        for n in nums:
            if n < 1 or n > self.sides():
                raise ValueError("value out of range in {0}".format(nums))
            for i in range(self.count(n) if maximum is None else min(maximum, self.count(n))):
                keep.append(n)
        return DiceRoll(keep, self.sides())


    def select_one(self, nums):
        ''' Returns the subroll of this roll that contains one occurrence
            of each of the given numbers that are also in this roll.

            nums -- a list of integers betwen 1 and the number of sides on the
                    dice in this roll
        '''
        keep = []
        # for each number in the list, add one of that number if the roll
        # contains at least one
        for n in nums:
            if n < 1 or n > self.sides():
                raise ValueError("value out of range in {0}".format(nums))
            if self.count(n) > 0:
                keep.append(n)
        return DiceRoll(keep, self.sides())


    def longest_runs(self):
        ''' Returns a list of all the longest consecutive runs in this
            roll.  For example, if this roll is [1 2 4 4 5] then the
            list returned is [[1, 2], [4, 5]].
        '''
        runs = []
        longest = 0
        curr_len = 0
        for i in range(1, self.sides() + 1):
            if self.count(i) > 0:
                curr_len += 1
                if curr_len == longest:
                    runs.append(list(range(i - curr_len + 1, i + 1)))
                elif curr_len > longest:
                    runs = [list(range(i - curr_len + 1, i + 1))]
                    longest = curr_len
            else:
                curr_len = 0
        return runs
        
        
    def __str__(self):
        ''' Returns a string representation of this roll.
        '''
        return str(self.as_list())


    def __repr__(self):
        return self.__str__()


    def __hash__(self):
        if self._hash is None:
            self._hash = (self.sides(), self.as_tuple()).__hash__()
        return self._hash

        
    def __eq__(self, other):
        ''' Determines if this roll is equal to the given other roll.
            Two rolls are equal if their dice show the same numbers
            and have the same number of sides.
        '''
        return self.sides() == other.sides() and self._dice == other._dice


    def __iter__(self):
        ''' Returns an iterator over the numbers showing in this roll.
        '''
        # delegate to the tuple representation
        return self.as_tuple().__iter__()