Portfolio: Dice and Yahtzees

By

View raw: dice.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# Copyright by Scott Severance
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

# This is a library to roll dice and find yahtzees.
#
# This module is compatible with Python 3. It is incompatible with Python 2.
'''Roll dice and find yahtzees'''

import random

class Die():
    '''This class represents a single die.'''

    def __init__(self, sides=6, value=None, use_PRNG=False):
        '''Create a single die.

        sides: The number of sides on the die.
        value: The initial value. Random if None.
        use_PRNG: Whether to use the pseudorandom number generator. Otherwise, use the system's best source of entropy.'''
        if sides < 2:
            raise ValueError('The die must have at least 2 sides.')
        self.sides = int(sides)
        self.use_PRNG = use_PRNG
        if use_PRNG:
            self._rand = random.randrange
        else:
            rand = random.SystemRandom()
            self._rand = rand.randrange
        if value is None:
            self.roll()
        else:
            if value != int(value):
                raise ValueError('The value must be an integer in the range 1-{}.'.format(sides))
            if 1 <= value <= sides:
                self.value = int(value)
            else:
                raise ValueError('The die has no side with the value "{}."'.format(value))

    def roll(self):
        '''Roll and return the die'''
        self.value = self._rand(1, self.sides + 1)
        return self

    def copy(self):
        '''Return a new Die with the same properties and value as self.'''
        return Die(sides=self.sides, value=self.value, use_PRNG=self.use_PRNG)

    def __print__(self):
        return str(self)

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return self.value == other

    def __ne__(self, other):
        return self.value != other

    def __gt__(self, other):
        return self.value > other

    def __ge__(self, other):
        return self.value >= other

    def __lt__(self, other):
        return self.value < other

    def __le__(self, other):
        return self.value <= other

    def __int__(self):
        return int(self.value)

    def __str__(self):
        return str('<{}>'.format(self.value))

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

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

    def __sub__(self, other):
        return self.value - other

    def __rsub__(self, other):
        return other - self.value

    def __mul__(self, other):
        return self.value * other

    def __rmul__(self, other):
        return other * self.value

    def __div__(self, other):
        return self.value.__div__(other)

    def __rdiv__(self, other):
        return other.__div__(self.value)

    def __floordiv__(self, other):
        return self.value.__floordiv__(other)

    def __rfloordiv__(self, other):
        return other.__floordiv__(self.value)

    def __mod__(self, other):
        return self.value % other

    def __rmod__(self, other):
        return other % self.value

    def __divmod__(self, other):
        return divmod(self.value, other)

    def __rdivmod__(self, other):
        return divmod(other, self.value)

class Cup(list):
    '''This class represents a cup containing dice. Since it is a subclass of list,
    the normal list methods and techniques work with this class.'''
    
    def __init__(self, *args, **kwargs):
        '''Create a cup of dice.

        - Pass the named argument 'num' with the number of dice to place in the
          cup.
        - Pass the argument 'sides' if you want all the dice to have a number of
          sides other than 6.
        - If you want different dice to have different numbers of sides, use the
          append() method to manually add dice.'''
        list.__init__(self, *args)
        sides = 6
        num = 0
        if 'sides' in kwargs:
            sides = kwargs['sides']
            del kwargs['sides']
        if 'num' in kwargs:
            num = kwargs['num']
            del kwargs['num']
        if num > 0:
            for i in range(num):
                list.append(self, Die(sides=sides, **kwargs))
        self._update()

    def _update(self):
        self.length = len(self)
        sides = [i.sides for i in self]
        try:
            self.min_sides = min(sides)
        except ValueError:
            self.min_sides = 0
        try:
            self.max_sides = max(sides)
        except ValueError:
            self.max_sides = 0

    def append(self, value):
        '''Add a die to the cup of dice'''
        if not isinstance(value, Die):
            raise TypeError('You may only add dice to the cup')
        list.append(self, value)
        self._update()
        return self

    def clear(self):
        '''L.clear() -> None -- remove all items from L'''
        list.clear(self)
        self._update()
        return self

    def copy(self):
        '''Create a new Cup containing new dice with the same properties, order, and value.'''
        c = Cup()
        for die in self:
            c.append(die.copy())
        return c

    def extend(self, iterable):
        '''L.extend(iterable) -> None -- extend list by appending elements from the iterable'''
        list.extend(self, iterable)
        self._update()
        return self

    def insert(self, index, die):
        '''L.insert(index, object) -- insert object before index'''
        if not isinstance(die, Die):
            raise TypeError('You may only add dice to the cup.')
        list.insert(self, index, die)
        self._update()
        return self

    def pop(self, index=None):
        '''L.pop([index]) -> item -- remove and return item at index (default last).
        Raises IndexError if list is empty or index is out of range.'''
        if index is None:
            index = self.length - 1
        r = list.pop(self, index)
        self._update()
        return r

    def remove(self, value):
        '''L.remove(value) -> None -- remove first occurrence of value.
        Raises ValueError if the value is not present.'''
        list.remove(self, value)
        self._update()

    def roll(self):
        '''Roll all the dice in the cup'''
        for die in self:
            die.roll()
        return self

    def __getitem__(self, key):
        '''Return the item in key. If key is a slice, a new Cup is returned.'''
        if isinstance(key, slice):
            lst = list.__getitem__(self, key)
            c = Cup()
            for die in lst:
                c.extend(die.copy())
            return c
        else:
            return list.__getitem__(self, key)

    def __setitem__(self, key, value):
        if not isinstance(value, Die):
            raise TypeError('You may only add dice to the cup')
        list.__setitem__(self, key, value)
        self._update()

    def __delitem__(self, key):
        '''Delete the item stored in key.'''
        list.__delitem__(self, key)
        self._update()

    def __add__(self, other):
        if not isinstance(other, Cup):
            raise NotImplementedError
        l = list.__add__(self, other)
        c = Cup()
        for die in l:
            c.append(die.copy())
        return c

    def __mul__(self, other):
        l = list.__mul__(self, other)
        c = Cup()
        for die in l:
            c.append(die.copy())
        return c

    def __rmul__(self, other):
        l = list.__rmul__(self, other)
        c = Cup()
        for die in l:
            c.append(die.copy())
        return c

    def is_yahtzee(self):
        '''Return True if all the dice have the same value'''
        for i in range(1, self.min_sides + 1):
            if self.count(i) == self.length:
                return True
        return False

    def roll_until_yahtzee(self):
        '''Repeatedly roll the dice until all the dice have the same value.

        Returns a tuple of the number of rolls it took and self.'''
        yahtzee = False
        count = 0
        self._update()
        while not yahtzee:
            self.roll()
            count += 1
            yahtzee = self.is_yahtzee()
        return count, self