"""
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, {})