Source code for thekraf.score

"""
thekraf.score
=============

Logic related to scoring dice and caching scores
"""
import json
import os
from itertools import combinations_with_replacement, combinations
import thekraf.config as cfg
import thekraf.db.models as mdls
from thekraf.diceutils import norm_dice, DiceUtils


[docs]class BaseScore(object): @classmethod
[docs] def gen_all_dice_strs(cls): """Generate all possible dice combos as strings Yields: str: Dice represented by a string """ dice_vals_str = ''.join(map(str, DiceUtils.VALS)) # Choose from all vals, then 1 less, then 2 less, ... for n in range(len(DiceUtils.VALS), 0, -1): # Choose from the possible values of a die for dice_chars in combinations_with_replacement(dice_vals_str, n): dice_str = ''.join(dice_chars) yield dice_str
@classmethod
[docs] def calc_of_a_kind(cls, opts, n, val): """Calculate score for n of a kind where n >= 3 Args: opts (thekraf.config.ScoreOptions): Scoring options n (int): Number of dice with `val` val (int): Value on the dice Returns: int: The score for `n` dice of value `val` """ assert 3 <= n <= cfg.GAME_DICE_MAX_COUNT result = opts.single1 * val if val != 1 else opts.triple1 if opts.fourplusscheme == 'set': if n >= 4: result = getattr(opts, 'kind' + str(n)) else: func = None if opts.fourplusscheme == 'add': func = result.__add__ elif opts.fourplusscheme == 'double': func = (2).__mul__ else: ValueError('Unknown scheme {}'.format( repr(opts.fourplusscheme) )) for _ in range(n - 3): result = func(result) return result
@classmethod @norm_dice(2) def score_all(cls, opts, dice): """Return score for all `dice` (0 if any of the dice are not scorable) Only returns non-zero if all dice are scoring Args: opts (thekraf.config.ScoreOptions): Scoring options dice (collections.Sequence[int]|str): Values of the dice to be scored Returns: int: The score """ result = 0 len_dice = len(dice) # Only count values included in dice counts = {die: dice.count(die) for die in set(dice)} # Special cases of all 6 dice interacting if len_dice == 6: # Two triplets if opts.twotriplets and list(counts.values()).count(3) == 2: result = opts.twotriplets # 4 of a kind and a pair elif (opts.kind4bonus and 4 in counts.values() and 2 in counts.values()): result = 0 for die, count in counts.items(): # 4 of a kind if count == 4: result += cls.calc_of_a_kind(opts, count, die) # Pair bonus else: result += opts.kind4bonus # Three pair elif opts.threepair and list(counts.values()).count(2) == 3: result = opts.threepair # Straight elif len(counts) == 6: result = opts.straight # 5 dice interacting if len_dice == 5: # Full house if (opts.fullhousebonus and 3 in counts.values() and 2 in counts.values()): for die, count in counts.items(): # Triplet if count == 3: result += cls.calc_of_a_kind(opts, count, die) # Pair bonus else: result += opts.fullhousebonus # Score dice, segregated by value if not result: chunks = [] for die, count in counts.items(): if count >= 3: chunks.append(cls.calc_of_a_kind(opts, count, die)) elif die == 1: chunks.append(count * opts.single1) elif die == 5: chunks.append(count * opts.single5) else: break if len(chunks) == len(counts): result = sum(chunks) return result
[docs]class ScoreCache(BaseScore): """Logic for creating and saving the score caches""" @classmethod
[docs] def create_score_cache(cls, opts): """Create cache of possible scores in which all dice score If any of the dice are non-scoring, the score would be zero, so it isn't in the cache (see examples). Args: opts (thekraf.config.ScoreOptions): Scoring options Returns: dict[str, int]: Dict from string rep of dice to score Examples: >>> d = [a for a in cfg.load_config_item('score_options') \ if a['name'] == 'default'][0] >>> opts = mdls.ScoreOptions(**d) >>> cache = ScoreCache.create_score_cache(opts) >>> cache['1'] 100 >>> '2' in cache False Since all dice have to be scoring... >>> '12' in cache False """ cache = {} for dice_str in cls.gen_all_dice_strs(): score = cls.score_all(opts, dice_str) if score: cache[dice_str] = score return cache
@classmethod
[docs] def create_score_any_cache(cls, score_cache): """Create cache of possible ways to score with sub-combos of dice Sub-combo also includes all of the dice. If none of the sub-combos are in :attr:`_score_cache`, it will not be included (see examples). Returns: dict[str, dict[str, int]: Dict from string rep of dice to sub-dict of :attr:`_score_cache` Raises: AssertionError: If :attr:`_score_cache` hasn't been initialized Examples: >>> d = [a for a in cfg.load_config_item('score_options') \ if a['name'] == 'default'][0] >>> opts = mdls.ScoreOptions(**d) >>> score_cache = ScoreCache.create_score_cache(opts) >>> cls = ScoreCache >>> cache = cls.create_score_any_cache(score_cache) >>> '1' in cache['12'] True >>> '2' in cache['12'] False >>> '12' in cache['12'] False >>> '11' in cache['11'] True >>> '1' in cache['1'] True """ if not score_cache: raise AssertionError(''.join([ 'The all scoring cache must be initialized before', ' initializing the any scoring cache.' ])) cache = {} for dice_str in cls.gen_all_dice_strs(): scoring_sub_combos = {} # Choose len dice, then (len - 1) dice, ... for n in range(len(dice_str), 0, -1): # Choose a sub-combo of dice for sub_dice_chars in combinations(dice_str, n): sub_dice_str = ''.join(sub_dice_chars) score = score_cache.get(sub_dice_str, 0) if score: scoring_sub_combos[sub_dice_str] = score if scoring_sub_combos: cache[dice_str] = scoring_sub_combos return cache
@classmethod
[docs] def create_score_any_by_num_cache(cls, score_any_cache): """Organize the score any cache by number of dice Examples: >>> d = cfg.load_config_item(\ 'score_options', select=lambda x: x['name'] == 'default')[0] >>> opts = mdls.ScoreOptions(**d) >>> cls = ScoreCache >>> cls.precache_scores(opts) >>> cache = opts.score_any_by_num_cache >>> sum(map(len, cache.values())) == len(opts.score_any_cache) True Args: score_any_cache (dict[str, dict[str, int]]): The score any cache Returns: dict[int, dict[str, dict[str, int]]]: Organized cache """ cache = {n: {dice_str: val for dice_str, val in score_any_cache.items() if len(dice_str) == n} for n in range(1, cfg.GAME_DICE_MAX_COUNT + 1)} return cache
@classmethod
[docs] def precache_scores(cls, opts): """Pre-calculate all possible scores This speeds up subsequent scoring and simplifies some logic Args: opts (thekraf.config.ScoreOptions): Scoring options """ opts.score_cache.clear() opts.score_any_cache.clear() opts.score_any_by_num_cache.clear() # Create the cache for all scoring dice opts.score_cache.update(cls.create_score_cache(opts=opts)) # Create the cache for any scoring dice opts.score_any_cache.update( cls.create_score_any_cache(opts.score_cache)) # Organize the cache for any scoring dice by number of dice opts.score_any_by_num_cache.update( cls.create_score_any_by_num_cache(opts.score_any_cache) )
@classmethod
[docs] def save_score_caches(cls, savepath): """Save the score caches to disk as json Args: savepath (str): The save path (should have an extension of 'json' """ with open(savepath, 'w') as fp: json.dump(cls._score_cache, fp, indent=0, sort_keys=True) prefix, ext = os.path.splitext(savepath) with open(prefix + '-any' + ext, 'w') as fp: json.dump(cls._score_any_cache, fp, indent=2, sort_keys=True)
[docs]class Score(ScoreCache, BaseScore): """Logic related to scoring dice""" @classmethod @norm_dice(2) def calc_score(cls, opts, dice, nocache=False): if nocache: return cls.score_all(dice) if not opts.score_cache: cls.precache_scores(opts) return opts.score_cache.get(DiceUtils.as_string(dice), 0) @classmethod @norm_dice(2) def subscores(cls, opts, dice): """Calculate all scoring sub-combos of dice Args: dice (tuple[int]): The dice to check for scoring sub-combos Returns: dict[str, dict[str, int]]: Dict of scoring sub-combos """ if not opts.score_any_cache: cls.precache_scores(opts) dice_str = DiceUtils.as_string(dice) return opts.score_any_cache.get(dice_str, {})