Portfolio: Dice and Yahtzees
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