"""
thekraf.diceutils
=================
Utilities for use with dice
"""
import collections
import logging
import random
import string
from itertools import combinations
import thekraf.config as cfg
logr = logging.getLogger('thk')
[docs]def norm_dice(arg_index=0):
"""Normalize any dice representations as true dice
Args:
arg_index (int): Index of the target arg in the parameter list.
Usually this would be the first arg, but it would be the second
if used on a class or instance method.
Returns:
Configured decorator
"""
def decorator(func):
def wrapped(*args, **kwargs):
mod_args = list(args)
# If dice was passed in as a kwarg
if 'dice' in kwargs:
kwargs['dice'] = DiceUtils.create_dice(kwargs['dice'])
# Modify the indicated argument
elif len(mod_args) > arg_index:
mod_args[arg_index] = DiceUtils.create_dice(mod_args[arg_index])
return func(*mod_args, **kwargs)
return wrapped
return decorator
[docs]class DiceUtils(object):
"""Utilities for use with/on dice"""
VALS = cfg.GAME_DICE_VALS
@staticmethod
[docs] def roll(n):
random.seed()
return tuple(random.choice(DiceUtils.VALS) for _ in range(n))
@staticmethod
[docs] def concat(dice, other):
"""Concatenate dice
Args:
dice (tuple[int]): Starting dice
other (tuple[int]): Dice to add on
Returns:
tuple[int]: Concatenated dice
"""
return sum((dice, other), ())
@staticmethod
[docs] def diff(minuend, subtrahend):
"""Return dice in minuend less the dice in the subtrahend
Args:
minuend (tuple[int]): Starting dice
subtrahend (tuple[int]): The dice to remove from `minuend`
Returns:
tuple[int]: The difference
"""
minuend = list(minuend)
# Note you can't use a comprehension because you might remove too much
# diff = [item for item in minuend if item not in subtrahend]
# Won't work --^
for item in subtrahend:
del minuend[minuend.index(item)]
return tuple(minuend)
@staticmethod
[docs] def is_dice_repr(obj):
"""True if `obj` appears to be a valid dice representation
Args:
obj (object): Object to test
Returns:
bool:
"""
# If string
if isinstance(obj, str):
return True
# If sequence of strings or ints
elif isinstance(obj, collections.Sequence):
if not obj or isinstance(obj[0], (int, str)):
return True
return False
@staticmethod
[docs] def is_dice_inst(obj):
"""True if `obj` is an instance of dice
Args:
obj (object): Object to test
Returns:
bool:
"""
if isinstance(obj, tuple):
if not obj or isinstance(obj[0], int):
return True
return False
@classmethod
[docs] def create_dice(cls, dice_repr):
"""Create dice values from various representations
Representation can be:
* string
* '123456'
* '1 2 3 4 5 6'
* '1,2,3,4,5,6'
* sequence of strings
* ['1', '1', '2', '2']
* sequence of int (basically passthrough)
* (3, 4, 5)
Args:
dice_repr (str): String to parse
Returns:
tuple[int]: Corresponding values
Raises:
TypeError: If `dice_repr` can't be converted to dice
ValueError: If dice values are not in :attr:`.VALS`
"""
# No conversion if already dice
if DiceUtils.is_dice_inst(dice_repr):
dice = tuple(dice_repr)
# Otherwise convert repr to dice
elif DiceUtils.is_dice_repr(dice_repr):
dice_repr = ''.join(map(str, dice_repr))
dice = tuple(int(c) for c in dice_repr if c in string.digits)
else:
raise TypeError('Cannot create dice from {}'.format(repr(dice_repr)))
if not all(die in cls.VALS for die in dice):
raise ValueError('Values {} -> {} must be in {}'.format(
repr(dice_repr),
repr(dice),
cls.VALS,
))
return dice
@staticmethod
[docs] def as_string(dice):
"""Convert dice to string representation
Dice are sorted before conversion so this also the key for the caches
Args:
dice (tuple[int]): Dice to convert
Returns:
str: Corresponding string
"""
assert DiceUtils.is_dice_inst(dice)
return ''.join(map(str, sorted(dice)))
@staticmethod
@norm_dice()
[docs] def indices_of(dice, subset):
"""Return the indices of the subset in dice
This requires more that a simple comprehension because items may be
repeated. A repeated item will search for its index after the index of
the previously found item.
Args:
dice (tuple[int]): Dice to search
subset (tuple[int]): Dice for which to search
Returns:
tuple[int]: The indices
"""
subset = DiceUtils.create_dice(subset)
indices = []
begins = {}
for die in subset:
i = dice.index(die, begins.get(die, 0))
indices.append(i)
# If this item is repeated, it will start searching after the
# current index
begins[die] = i + 1
return tuple(indices)
# class Action(object):
#
# def __init__(self, dice, scoring_dice, score=None):
# self.dice = dice
# self.scoring_dice = scoring_dice
# if score is None:
# self.score = calc_score(self.dice)
# else:
# self.score = score
#
# ACTION_DICT = {}
#
#
# def possible_actions(unsorted_dice):
# dice = sorted(unsorted_dice)
# key = ''.join(str(d) for d in dice)
# logr.debug('key: "{}"'.format(key))
# if key in ACTION_DICT:
# logr.debug('key already in dict')
# return ACTION_DICT[key]
#
# actions = []
# for count in range(1, len(dice) + 1)[::-1]:
# for combo in combinations(dice, count):
# action = Action(dice, combo)
# if action.score:
# actions.append(action)